feat(store)!: Lock AuthDir when use gitstore/pgstore

This commit is contained in:
hkfires
2025-10-14 10:46:45 +08:00
parent d6aa1e5ba0
commit 78989d6c0d
5 changed files with 48 additions and 31 deletions

2
.gitignore vendored
View File

@@ -16,3 +16,5 @@ temp/*
cli-proxy-api cli-proxy-api
static/* static/*
.env .env
pgstore/*
gitstore/*

View File

@@ -447,12 +447,12 @@ You can also persist configuration and authentication data in PostgreSQL when ru
| `MANAGEMENT_PASSWORD` | Yes | | Password for the management web UI (required when remote management is enabled). | | `MANAGEMENT_PASSWORD` | Yes | | Password for the management web UI (required when remote management is enabled). |
| `PGSTORE_DSN` | Yes | | PostgreSQL connection string (e.g. `postgresql://user:pass@host:5432/db`). | | `PGSTORE_DSN` | Yes | | PostgreSQL connection string (e.g. `postgresql://user:pass@host:5432/db`). |
| `PGSTORE_SCHEMA` | No | public | Schema where the tables will be created. Leave empty to use the default schema. | | `PGSTORE_SCHEMA` | No | public | Schema where the tables will be created. Leave empty to use the default schema. |
| `PGSTORE_CACHE_DIR` | No | Current working directory | Root directory for the local mirror; the server writes to `<value>/pgstore`. If unset and CWD is unavailable, `/tmp/pgstore` is used. | | `PGSTORE_LOCAL_PATH` | No | Current working directory | Root directory for the local mirror; the server writes to `<value>/pgstore`. If unset and CWD is unavailable, `/tmp/pgstore` is used. |
**How it Works** **How it Works**
1. **Initialization:** On startup the server connects via `PGSTORE_DSN`, ensures the schema exists, and creates the `config_store` / `auth_store` tables when missing. 1. **Initialization:** On startup the server connects via `PGSTORE_DSN`, ensures the schema exists, and creates the `config_store` / `auth_store` tables when missing.
2. **Local Mirror:** A writable cache at `<PGSTORE_CACHE_DIR or CWD>/pgstore` mirrors `config/config.yaml` and `auths/` so the rest of the application can reuse the existing file-based logic. 2. **Local Mirror:** A writable cache at `<PGSTORE_LOCAL_PATH or CWD>/pgstore` mirrors `config/config.yaml` and `auths/` so the rest of the application can reuse the existing file-based logic.
3. **Bootstrapping:** If no configuration row exists, `config.example.yaml` seeds the database using the fixed identifier `config`. 3. **Bootstrapping:** If no configuration row exists, `config.example.yaml` seeds the database using the fixed identifier `config`.
4. **Token Sync:** Changes flow both ways—file updates are written to PostgreSQL and database records are mirrored back to disk so watchers and management APIs continue to operate. 4. **Token Sync:** Changes flow both ways—file updates are written to PostgreSQL and database records are mirrored back to disk so watchers and management APIs continue to operate.

View File

@@ -460,12 +460,12 @@ openai-compatibility:
| `MANAGEMENT_PASSWORD` | 是 | | 管理面板密码(启用远程管理时必需)。 | | `MANAGEMENT_PASSWORD` | 是 | | 管理面板密码(启用远程管理时必需)。 |
| `PGSTORE_DSN` | 是 | | PostgreSQL 连接串,例如 `postgresql://user:pass@host:5432/db`。 | | `PGSTORE_DSN` | 是 | | PostgreSQL 连接串,例如 `postgresql://user:pass@host:5432/db`。 |
| `PGSTORE_SCHEMA` | 否 | public | 创建表时使用的 schema留空则使用默认 schema。 | | `PGSTORE_SCHEMA` | 否 | public | 创建表时使用的 schema留空则使用默认 schema。 |
| `PGSTORE_CACHE_DIR` | 否 | 当前工作目录 | 本地镜像根目录,服务将在 `<值>/pgstore` 下写入缓存;若无法获取工作目录则退回 `/tmp/pgstore`。 | | `PGSTORE_LOCAL_PATH` | 否 | 当前工作目录 | 本地镜像根目录,服务将在 `<值>/pgstore` 下写入缓存;若无法获取工作目录则退回 `/tmp/pgstore`。 |
**工作原理** **工作原理**
1. **初始化:** 启动时通过 `PGSTORE_DSN` 连接数据库,确保 schema 存在,并在缺失时创建 `config_store` 与 `auth_store`。 1. **初始化:** 启动时通过 `PGSTORE_DSN` 连接数据库,确保 schema 存在,并在缺失时创建 `config_store` 与 `auth_store`。
2. **本地镜像:** 在 `<PGSTORE_CACHE_DIR 或当前工作目录>/pgstore` 下建立可写缓存,复用 `config/config.yaml` 与 `auths/` 目录。 2. **本地镜像:** 在 `<PGSTORE_LOCAL_PATH 或当前工作目录>/pgstore` 下建立可写缓存,复用 `config/config.yaml` 与 `auths/` 目录。
3. **引导:** 若数据库中无配置记录,会使用 `config.example.yaml` 初始化,并以固定标识 `config` 写入。 3. **引导:** 若数据库中无配置记录,会使用 `config.example.yaml` 初始化,并以固定标识 `config` 写入。
4. **令牌同步:** 配置与令牌的更改会写入 PostgreSQL同时数据库中的内容也会反向同步至本地镜像便于文件监听与管理接口继续工作。 4. **令牌同步:** 配置与令牌的更改会写入 PostgreSQL同时数据库中的内容也会反向同步至本地镜像便于文件监听与管理接口继续工作。

View File

@@ -105,7 +105,7 @@ func main() {
usePostgresStore bool usePostgresStore bool
pgStoreDSN string pgStoreDSN string
pgStoreSchema string pgStoreSchema string
pgStoreCacheDir string pgStoreLocalPath string
pgStoreInst *store.PostgresStore pgStoreInst *store.PostgresStore
gitStoreLocalPath string gitStoreLocalPath string
useGitStore bool useGitStore bool
@@ -139,8 +139,8 @@ func main() {
if value, ok := lookupEnv("PGSTORE_SCHEMA", "pgstore_schema"); ok { if value, ok := lookupEnv("PGSTORE_SCHEMA", "pgstore_schema"); ok {
pgStoreSchema = value pgStoreSchema = value
} }
if value, ok := lookupEnv("PGSTORE_CACHE_DIR", "pgstore_cache_dir"); ok { if value, ok := lookupEnv("PGSTORE_LOCAL_PATH", "pgstore_local_path"); ok {
pgStoreCacheDir = value pgStoreLocalPath = value
} }
useGitStore = false useGitStore = false
} }
@@ -169,15 +169,15 @@ func main() {
// Prefer the Postgres store when configured, otherwise fallback to git or local files. // Prefer the Postgres store when configured, otherwise fallback to git or local files.
var configFilePath string var configFilePath string
if usePostgresStore { if usePostgresStore {
if pgStoreCacheDir == "" { if pgStoreLocalPath == "" {
pgStoreCacheDir = wd pgStoreLocalPath = wd
} }
pgStoreCacheDir = filepath.Join(pgStoreCacheDir, "pgstore") pgStoreLocalPath = filepath.Join(pgStoreLocalPath, "pgstore")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
pgStoreInst, err = store.NewPostgresStore(ctx, store.PostgresStoreConfig{ pgStoreInst, err = store.NewPostgresStore(ctx, store.PostgresStoreConfig{
DSN: pgStoreDSN, DSN: pgStoreDSN,
Schema: pgStoreSchema, Schema: pgStoreSchema,
SpoolDir: pgStoreCacheDir, SpoolDir: pgStoreLocalPath,
}) })
cancel() cancel()
if err != nil { if err != nil {

View File

@@ -35,6 +35,10 @@ type storePersister interface {
PersistAuthFiles(ctx context.Context, message string, paths ...string) error PersistAuthFiles(ctx context.Context, message string, paths ...string) error
} }
type authDirProvider interface {
AuthDir() string
}
// Watcher manages file watching for configuration and authentication files // Watcher manages file watching for configuration and authentication files
type Watcher struct { type Watcher struct {
configPath string configPath string
@@ -53,6 +57,7 @@ type Watcher struct {
pendingOrder []string pendingOrder []string
dispatchCancel context.CancelFunc dispatchCancel context.CancelFunc
storePersister storePersister storePersister storePersister
mirroredAuthDir string
oldConfigYaml []byte oldConfigYaml []byte
} }
@@ -130,6 +135,12 @@ func NewWatcher(configPath, authDir string, reloadCallback func(*config.Config))
w.storePersister = persister w.storePersister = persister
log.Debug("persistence-capable token store detected; watcher will propagate persisted changes") log.Debug("persistence-capable token store detected; watcher will propagate persisted changes")
} }
if provider, ok := store.(authDirProvider); ok {
if fixed := strings.TrimSpace(provider.AuthDir()); fixed != "" {
w.mirroredAuthDir = fixed
log.Debugf("mirrored auth directory locked to %s", fixed)
}
}
} }
return w, nil return w, nil
} }
@@ -517,11 +528,15 @@ func (w *Watcher) reloadConfig() bool {
return false return false
} }
if w.mirroredAuthDir != "" {
newConfig.AuthDir = w.mirroredAuthDir
} else {
if resolvedAuthDir, errResolveAuthDir := util.ResolveAuthDir(newConfig.AuthDir); errResolveAuthDir != nil { if resolvedAuthDir, errResolveAuthDir := util.ResolveAuthDir(newConfig.AuthDir); errResolveAuthDir != nil {
log.Errorf("failed to resolve auth directory from config: %v", errResolveAuthDir) log.Errorf("failed to resolve auth directory from config: %v", errResolveAuthDir)
} else { } else {
newConfig.AuthDir = resolvedAuthDir newConfig.AuthDir = resolvedAuthDir
} }
}
w.clientsMutex.Lock() w.clientsMutex.Lock()
var oldConfig *config.Config var oldConfig *config.Config