mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-07 18:26:37 +08:00
Merge pull request #96 from Sagit-chu/opencode/glowing-orchid
feat(postgres): add migration workflow and id self-heal
This commit is contained in:
@@ -44,7 +44,7 @@ curl -L https://github.com/Sagit-chu/flux-panel/releases/download/2.1.0/install.
|
|||||||
|
|
||||||
#### PostgreSQL 部署(Docker Compose)
|
#### PostgreSQL 部署(Docker Compose)
|
||||||
|
|
||||||
`docker-compose-v4.yml` / `docker-compose-v6.yml` 已包含 PostgreSQL 服务。默认仍使用 SQLite,切换到 PostgreSQL 只需要配置环境变量。
|
安装脚本会根据环境自动下载对应的 Compose 配置并保存为 `docker-compose.yml`。默认仍使用 SQLite,切换到 PostgreSQL 只需要配置环境变量。
|
||||||
|
|
||||||
1) 在 `docker-compose` 同目录创建或修改 `.env`:
|
1) 在 `docker-compose` 同目录创建或修改 `.env`:
|
||||||
|
|
||||||
@@ -61,47 +61,56 @@ POSTGRES_USER=flux_panel
|
|||||||
POSTGRES_PASSWORD=replace_with_strong_password
|
POSTGRES_PASSWORD=replace_with_strong_password
|
||||||
```
|
```
|
||||||
|
|
||||||
2) 启动(IPv4/IPv6 二选一):
|
> 📌 使用安装脚本部署时,`POSTGRES_PASSWORD` 会自动随机生成并写入 `.env`。
|
||||||
|
|
||||||
|
2) 启动服务:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose-v4.yml up -d
|
docker compose up -d
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f docker-compose-v6.yml up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3) 如果你想继续使用 SQLite,保留 `DB_TYPE=sqlite`(或不设置 `DB_TYPE`)即可。
|
3) 如果你想继续使用 SQLite,保留 `DB_TYPE=sqlite`(或不设置 `DB_TYPE`)即可。
|
||||||
|
|
||||||
#### 从 SQLite 迁移到 PostgreSQL
|
#### 从 SQLite 迁移到 PostgreSQL
|
||||||
|
|
||||||
以下示例基于 Docker Volume `sqlite_data`(项目默认配置)与 `pgloader`:
|
如果你是通过 `panel_install.sh` 安装面板,推荐直接使用脚本菜单一键迁移:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./panel_install.sh
|
||||||
|
# 选择 4. 迁移到 PostgreSQL
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本会自动完成 SQLite 备份、PostgreSQL 启动、`pgloader` 导入、`.env` 中 `DB_TYPE`/`DATABASE_URL` 更新,并重启服务。
|
||||||
|
|
||||||
|
如果你希望手动迁移,以下示例基于 Docker Volume `sqlite_data`(项目默认配置)与 `pgloader`:
|
||||||
|
|
||||||
1) 停止服务并备份 SQLite 数据:
|
1) 停止服务并备份 SQLite 数据:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose-v4.yml down
|
docker compose down
|
||||||
docker run --rm -v sqlite_data:/data -v "$(pwd)":/backup alpine sh -c "cp /data/gost.db /backup/gost.db.bak"
|
docker run --rm -v sqlite_data:/data -v "$(pwd)":/backup alpine sh -c "cp /data/gost.db /backup/gost.db.bak"
|
||||||
```
|
```
|
||||||
|
|
||||||
2) 仅启动 PostgreSQL:
|
2) 仅启动 PostgreSQL:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose-v4.yml up -d postgres
|
docker compose up -d postgres
|
||||||
```
|
```
|
||||||
|
|
||||||
3) 使用 `pgloader` 迁移:
|
3) 使用 `pgloader` 迁移:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run --rm --network gost-network -v sqlite_data:/sqlite dimitri/pgloader:latest pgloader /sqlite/gost.db postgresql://flux_panel:replace_with_strong_password@postgres:5432/flux_panel
|
source .env
|
||||||
|
docker run --rm --network gost-network -v sqlite_data:/sqlite dimitri/pgloader:latest pgloader /sqlite/gost.db "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}"
|
||||||
```
|
```
|
||||||
|
|
||||||
4) 切换后端到 PostgreSQL 并启动:
|
4) 切换后端到 PostgreSQL 并启动:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
source .env
|
||||||
export DB_TYPE=postgres
|
export DB_TYPE=postgres
|
||||||
export DATABASE_URL="postgres://flux_panel:replace_with_strong_password@postgres:5432/flux_panel?sslmode=disable"
|
export DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable"
|
||||||
docker compose -f docker-compose-v4.yml up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
5) 迁移完成后,登录面板检查用户、隧道、转发、节点数据是否正确。
|
5) 迁移完成后,登录面板检查用户、隧道、转发、节点数据是否正确。
|
||||||
@@ -138,7 +147,7 @@ The following major changes and additions have been made in this fork (FLVX):
|
|||||||
- **Removed**: `ios-app/` - Source code for the iOS client.
|
- **Removed**: `ios-app/` - Source code for the iOS client.
|
||||||
|
|
||||||
### 5. Infrastructure & Scripts
|
### 5. Infrastructure & Scripts
|
||||||
- **Modified**: `docker-compose-v4.yml`, `docker-compose-v6.yml` (Updated for Go backend).
|
- **Modified**: `docker-compose.yml` (installer output name, auto-selects IPv4/IPv6 template, updated for Go backend).
|
||||||
- **Modified**: `install.sh`, `panel_install.sh` (Updated installation logic).
|
- **Modified**: `install.sh`, `panel_install.sh` (Updated installation logic).
|
||||||
- **Added**: `AGENTS.md` (Project documentation).
|
- **Added**: `AGENTS.md` (Project documentation).
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@
|
|||||||
**A**: 面板安装脚本会自动尝试配置 Docker 的 IPv6。如果失败,请手动检查 `/etc/docker/daemon.json` 配置,确保 `ipv6: true` 且分配了正确的 `fixed-cidr-v6` 子网。
|
**A**: 面板安装脚本会自动尝试配置 Docker 的 IPv6。如果失败,请手动检查 `/etc/docker/daemon.json` 配置,确保 `ipv6: true` 且分配了正确的 `fixed-cidr-v6` 子网。
|
||||||
|
|
||||||
### Q6: 如何切换到 PostgreSQL?
|
### Q6: 如何切换到 PostgreSQL?
|
||||||
**A**: 在 `.env` 文件中设置 `DB_TYPE=postgres` 并填写相关配置,然后重启服务即可。各变量默认值:`DATABASE_URL=postgres://flux_panel:replace_with_strong_password@postgres:5432/flux_panel?sslmode=disable`、`POSTGRES_DB=flux_panel`、`POSTGRES_USER=flux_panel`,`POSTGRES_PASSWORD` 需自行设置强密码。详见 [PostgreSQL 数据库指南](./postgresql.md)。
|
**A**: 在 `.env` 文件中设置 `DB_TYPE=postgres`,并让 `DATABASE_URL` 与 `POSTGRES_*` 保持一致,然后执行 `docker compose up -d` 重启服务即可。使用安装脚本部署时,`POSTGRES_PASSWORD` 会自动随机生成并写入 `.env`。详见 [PostgreSQL 数据库指南](./postgresql.md)。
|
||||||
|
|
||||||
### Q7: 从 SQLite 迁移到 PostgreSQL 后数据丢失?
|
### Q7: 从 SQLite 迁移到 PostgreSQL 后数据丢失?
|
||||||
**A**:
|
**A**:
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ curl -L https://raw.githubusercontent.com/Sagit-chu/flux-panel/main/panel_instal
|
|||||||
1. 安装面板
|
1. 安装面板
|
||||||
2. 更新面板
|
2. 更新面板
|
||||||
3. 卸载面板
|
3. 卸载面板
|
||||||
|
4. 迁移到 PostgreSQL
|
||||||
|
5. 退出
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+30
-14
@@ -42,7 +42,7 @@ FLVX 默认使用 SQLite 作为数据库,同时也内置了对 PostgreSQL 的
|
|||||||
|
|
||||||
## 三、全新部署(Docker Compose + PostgreSQL)
|
## 三、全新部署(Docker Compose + PostgreSQL)
|
||||||
|
|
||||||
`docker-compose-v4.yml` / `docker-compose-v6.yml` 已包含 PostgreSQL 服务,默认使用 SQLite。只需配置环境变量即可切换到 PostgreSQL。
|
安装脚本会根据环境自动下载对应的 Compose 配置并保存为 `docker-compose.yml`。默认使用 SQLite,只需配置环境变量即可切换到 PostgreSQL。
|
||||||
|
|
||||||
### 1. 创建 `.env` 文件
|
### 1. 创建 `.env` 文件
|
||||||
|
|
||||||
@@ -67,14 +67,8 @@ POSTGRES_PASSWORD=替换为强密码
|
|||||||
|
|
||||||
### 2. 启动服务
|
### 2. 启动服务
|
||||||
|
|
||||||
IPv4 环境:
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose-v4.yml up -d
|
docker compose up -d
|
||||||
```
|
|
||||||
|
|
||||||
IPv6 环境:
|
|
||||||
```bash
|
|
||||||
docker compose -f docker-compose-v6.yml up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 验证
|
### 3. 验证
|
||||||
@@ -96,11 +90,29 @@ docker logs flux-panel-postgres
|
|||||||
|
|
||||||
如果你已经在使用 SQLite 并且希望迁移到 PostgreSQL,请按照以下步骤操作。
|
如果你已经在使用 SQLite 并且希望迁移到 PostgreSQL,请按照以下步骤操作。
|
||||||
|
|
||||||
|
### 快速方式:脚本菜单一键迁移(推荐)
|
||||||
|
|
||||||
|
如果你是通过安装脚本部署面板,可直接执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./panel_install.sh
|
||||||
|
# 选择 4. 迁移到 PostgreSQL
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本会自动完成以下操作:
|
||||||
|
- 备份 SQLite 数据到当前目录(`gost.db.bak`)
|
||||||
|
- 启动并等待 PostgreSQL 健康检查通过
|
||||||
|
- 使用 `pgloader` 导入 SQLite 数据
|
||||||
|
- 自动写入 `.env` 的 `DB_TYPE=postgres` 与 `DATABASE_URL`
|
||||||
|
- 重启服务并等待后端健康检查
|
||||||
|
|
||||||
|
### 手动方式:按步骤迁移
|
||||||
|
|
||||||
### 1. 备份 SQLite 数据
|
### 1. 备份 SQLite 数据
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 停止所有服务
|
# 停止所有服务
|
||||||
docker compose -f docker-compose-v4.yml down
|
docker compose down
|
||||||
|
|
||||||
# 备份 SQLite 数据文件到当前目录
|
# 备份 SQLite 数据文件到当前目录
|
||||||
docker run --rm -v sqlite_data:/data -v "$(pwd)":/backup alpine sh -c "cp /data/gost.db /backup/gost.db.bak"
|
docker run --rm -v sqlite_data:/data -v "$(pwd)":/backup alpine sh -c "cp /data/gost.db /backup/gost.db.bak"
|
||||||
@@ -113,7 +125,7 @@ docker run --rm -v sqlite_data:/data -v "$(pwd)":/backup alpine sh -c "cp /data/
|
|||||||
### 3. 仅启动 PostgreSQL
|
### 3. 仅启动 PostgreSQL
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose-v4.yml up -d postgres
|
docker compose up -d postgres
|
||||||
```
|
```
|
||||||
|
|
||||||
等待 PostgreSQL 完全就绪:
|
等待 PostgreSQL 完全就绪:
|
||||||
@@ -127,19 +139,23 @@ docker inspect --format='{{.State.Health.Status}}' flux-panel-postgres
|
|||||||
### 4. 使用 pgloader 迁移数据
|
### 4. 使用 pgloader 迁移数据
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
source .env
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
--network gost-network \
|
--network gost-network \
|
||||||
-v sqlite_data:/sqlite \
|
-v sqlite_data:/sqlite \
|
||||||
dimitri/pgloader:latest \
|
dimitri/pgloader:latest \
|
||||||
pgloader /sqlite/gost.db postgresql://flux_panel:你的密码@postgres:5432/flux_panel
|
pgloader /sqlite/gost.db "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}"
|
||||||
```
|
```
|
||||||
|
|
||||||
> 📌 将 `你的密码` 替换为实际的 `POSTGRES_PASSWORD`。
|
> 📌 建议直接从 `.env` 读取 `POSTGRES_USER`、`POSTGRES_PASSWORD`、`POSTGRES_DB`,避免手填密码导致认证失败。
|
||||||
|
|
||||||
### 5. 启动全部服务
|
### 5. 启动全部服务
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose-v4.yml up -d
|
source .env
|
||||||
|
export DB_TYPE=postgres
|
||||||
|
export DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable"
|
||||||
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. 验证迁移
|
### 6. 验证迁移
|
||||||
@@ -182,7 +198,7 @@ DATABASE_URL=postgres://flux_panel:你的强密码@数据库地址:5432/flux_pan
|
|||||||
如果使用外部 PostgreSQL,可以在启动时不启动内置的 postgres 服务:
|
如果使用外部 PostgreSQL,可以在启动时不启动内置的 postgres 服务:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose-v4.yml up -d backend frontend
|
docker compose up -d backend frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1345,9 +1345,165 @@ func migrateSchema(db *store.DB) error {
|
|||||||
ensureColumn(table, col, typ)
|
ensureColumn(table, col, typ)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if db.Dialect() == store.DialectPostgres {
|
||||||
|
if err := ensurePostgresIDDefaults(db); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensurePostgresIDDefaults(db *store.DB) error {
|
||||||
|
rows, err := db.Query(`
|
||||||
|
SELECT c.table_schema, c.table_name
|
||||||
|
FROM information_schema.table_constraints tc
|
||||||
|
JOIN information_schema.key_column_usage kcu
|
||||||
|
ON tc.constraint_name = kcu.constraint_name
|
||||||
|
AND tc.table_schema = kcu.table_schema
|
||||||
|
JOIN information_schema.columns c
|
||||||
|
ON c.table_schema = kcu.table_schema
|
||||||
|
AND c.table_name = kcu.table_name
|
||||||
|
AND c.column_name = kcu.column_name
|
||||||
|
WHERE tc.constraint_type = 'PRIMARY KEY'
|
||||||
|
AND kcu.column_name = 'id'
|
||||||
|
AND c.data_type IN ('integer', 'bigint')
|
||||||
|
AND c.is_identity = 'NO'
|
||||||
|
AND c.table_schema = current_schema()
|
||||||
|
ORDER BY c.table_name ASC
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("discover postgres id columns: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var schemaName string
|
||||||
|
var tableName string
|
||||||
|
if err := rows.Scan(&schemaName, &tableName); err != nil {
|
||||||
|
return fmt.Errorf("scan postgres id table row: %w", err)
|
||||||
|
}
|
||||||
|
if err := ensurePostgresTableIDDefault(db, schemaName, tableName); err != nil {
|
||||||
|
return fmt.Errorf("repair %s.%s id default: %w", schemaName, tableName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return fmt.Errorf("iterate postgres id tables: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensurePostgresTableIDDefault(db *store.DB, schemaName, tableName string) error {
|
||||||
|
var defaultExpr sql.NullString
|
||||||
|
if err := db.QueryRow(`
|
||||||
|
SELECT column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = ?
|
||||||
|
AND table_name = ?
|
||||||
|
AND column_name = 'id'
|
||||||
|
LIMIT 1
|
||||||
|
`, schemaName, tableName).Scan(&defaultExpr); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNextvalDefault := defaultExpr.Valid && strings.Contains(strings.ToLower(defaultExpr.String), "nextval(")
|
||||||
|
|
||||||
|
var serialSeq sql.NullString
|
||||||
|
if err := db.QueryRow(`
|
||||||
|
SELECT pg_get_serial_sequence(format('%I.%I', ?, ?), 'id')
|
||||||
|
`, schemaName, tableName).Scan(&serialSeq); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
seqRef := strings.TrimSpace(serialSeq.String)
|
||||||
|
if seqRef == "" && hasNextvalDefault {
|
||||||
|
seqRef = extractNextvalRegclass(defaultExpr.String)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasNextvalDefault || seqRef == "" {
|
||||||
|
seqName := tableName + "_id_seq"
|
||||||
|
if _, err := db.Exec(fmt.Sprintf("CREATE SEQUENCE IF NOT EXISTS %s.%s", quoteSQLIdentifier(schemaName), quoteSQLIdentifier(seqName))); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
seqRef = schemaName + "." + seqName
|
||||||
|
if _, err := db.Exec(fmt.Sprintf(
|
||||||
|
"ALTER TABLE %s.%s ALTER COLUMN id SET DEFAULT nextval(%s::regclass)",
|
||||||
|
quoteSQLIdentifier(schemaName),
|
||||||
|
quoteSQLIdentifier(tableName),
|
||||||
|
quoteSQLLiteral(seqRef),
|
||||||
|
)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := db.Exec(fmt.Sprintf(
|
||||||
|
"ALTER SEQUENCE %s.%s OWNED BY %s.%s.id",
|
||||||
|
quoteSQLIdentifier(schemaName),
|
||||||
|
quoteSQLIdentifier(seqName),
|
||||||
|
quoteSQLIdentifier(schemaName),
|
||||||
|
quoteSQLIdentifier(tableName),
|
||||||
|
)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return syncPostgresTableIDSequence(db, schemaName, tableName, seqRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncPostgresTableIDSequence(db *store.DB, schemaName, tableName, seqRef string) error {
|
||||||
|
var maxID int64
|
||||||
|
if err := db.QueryRow(fmt.Sprintf(
|
||||||
|
"SELECT COALESCE(MAX(id), 0) FROM %s.%s",
|
||||||
|
quoteSQLIdentifier(schemaName),
|
||||||
|
quoteSQLIdentifier(tableName),
|
||||||
|
)).Scan(&maxID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
setVal := maxID
|
||||||
|
isCalled := true
|
||||||
|
if maxID <= 0 {
|
||||||
|
setVal = 1
|
||||||
|
isCalled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := db.Exec(`SELECT setval(?::regclass, ?, ?)`, seqRef, setVal, isCalled); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractNextvalRegclass(defaultExpr string) string {
|
||||||
|
nextvalIdx := strings.Index(strings.ToLower(defaultExpr), "nextval(")
|
||||||
|
if nextvalIdx < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
expr := defaultExpr[nextvalIdx:]
|
||||||
|
firstQuote := strings.Index(expr, "'")
|
||||||
|
if firstQuote < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
expr = expr[firstQuote+1:]
|
||||||
|
secondQuote := strings.Index(expr, "'")
|
||||||
|
if secondQuote < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(expr[:secondQuote])
|
||||||
|
}
|
||||||
|
|
||||||
|
func quoteSQLIdentifier(ident string) string {
|
||||||
|
return `"` + strings.ReplaceAll(ident, `"`, `""`) + `"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func quoteSQLLiteral(value string) string {
|
||||||
|
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
|
||||||
|
}
|
||||||
|
|
||||||
func isMissingColumnError(dialect store.Dialect, err error) bool {
|
func isMissingColumnError(dialect store.Dialect, err error) bool {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return false
|
return false
|
||||||
|
|||||||
+234
-34
@@ -203,7 +203,8 @@ show_menu() {
|
|||||||
echo "1. 安装面板"
|
echo "1. 安装面板"
|
||||||
echo "2. 更新面板"
|
echo "2. 更新面板"
|
||||||
echo "3. 卸载面板"
|
echo "3. 卸载面板"
|
||||||
echo "4. 退出"
|
echo "4. 迁移到 PostgreSQL"
|
||||||
|
echo "5. 退出"
|
||||||
echo "==============================================="
|
echo "==============================================="
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +233,90 @@ upsert_env_var() {
|
|||||||
mv "$tmp_file" "$file"
|
mv "$tmp_file" "$file"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_env_var() {
|
||||||
|
local key="$1"
|
||||||
|
local file="${2:-.env}"
|
||||||
|
|
||||||
|
if [[ ! -f "$file" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
grep -m1 "^${key}=" "$file" | cut -d= -f2-
|
||||||
|
}
|
||||||
|
|
||||||
|
get_current_db_type() {
|
||||||
|
local db_type database_url
|
||||||
|
|
||||||
|
db_type=$(get_env_var "DB_TYPE")
|
||||||
|
database_url=$(get_env_var "DATABASE_URL")
|
||||||
|
|
||||||
|
if [[ "$db_type" == "postgres" || "$database_url" == postgres://* || "$database_url" == postgresql://* ]]; then
|
||||||
|
echo "postgres"
|
||||||
|
else
|
||||||
|
echo "sqlite"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_postgres_healthy() {
|
||||||
|
local pg_health
|
||||||
|
|
||||||
|
echo "🔍 检查 PostgreSQL 服务状态..."
|
||||||
|
for i in {1..90}; do
|
||||||
|
if docker ps --format "{{.Names}}" | grep -q "^flux-panel-postgres$"; then
|
||||||
|
pg_health=$(docker inspect -f '{{.State.Health.Status}}' flux-panel-postgres 2>/dev/null || echo "unknown")
|
||||||
|
if [[ "$pg_health" == "healthy" ]]; then
|
||||||
|
echo "✅ PostgreSQL 服务健康检查通过"
|
||||||
|
return 0
|
||||||
|
elif [[ "$pg_health" == "unhealthy" ]]; then
|
||||||
|
echo "⚠️ PostgreSQL 健康状态:$pg_health"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
pg_health="not_running"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $i -eq 90 ]; then
|
||||||
|
echo "❌ PostgreSQL 启动超时(90秒)"
|
||||||
|
echo "🔍 当前状态:$(docker inspect -f '{{.State.Health.Status}}' flux-panel-postgres 2>/dev/null || echo '容器不存在')"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $((i % 15)) -eq 1 ]; then
|
||||||
|
echo "⏳ 等待 PostgreSQL 启动... ($i/90) 状态:${pg_health:-unknown}"
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_backend_healthy() {
|
||||||
|
local backend_health
|
||||||
|
|
||||||
|
echo "🔍 检查后端服务状态..."
|
||||||
|
for i in {1..90}; do
|
||||||
|
if docker ps --format "{{.Names}}" | grep -q "^flux-panel-backend$"; then
|
||||||
|
backend_health=$(docker inspect -f '{{.State.Health.Status}}' flux-panel-backend 2>/dev/null || echo "unknown")
|
||||||
|
if [[ "$backend_health" == "healthy" ]]; then
|
||||||
|
echo "✅ 后端服务健康检查通过"
|
||||||
|
return 0
|
||||||
|
elif [[ "$backend_health" == "unhealthy" ]]; then
|
||||||
|
echo "⚠️ 后端健康状态:$backend_health"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
backend_health="not_running"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $i -eq 90 ]; then
|
||||||
|
echo "❌ 后端服务启动超时(90秒)"
|
||||||
|
echo "🔍 当前状态:$(docker inspect -f '{{.State.Health.Status}}' flux-panel-backend 2>/dev/null || echo '容器不存在')"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $((i % 15)) -eq 1 ]; then
|
||||||
|
echo "⏳ 等待后端服务启动... ($i/90) 状态:${backend_health:-unknown}"
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# 删除脚本自身
|
# 删除脚本自身
|
||||||
delete_self() {
|
delete_self() {
|
||||||
echo ""
|
echo ""
|
||||||
@@ -253,6 +338,33 @@ get_config_params() {
|
|||||||
read -p "后端端口(默认 6365): " BACKEND_PORT
|
read -p "后端端口(默认 6365): " BACKEND_PORT
|
||||||
BACKEND_PORT=${BACKEND_PORT:-6365}
|
BACKEND_PORT=${BACKEND_PORT:-6365}
|
||||||
|
|
||||||
|
echo "请选择数据库类型:"
|
||||||
|
echo "1. SQLite(默认)"
|
||||||
|
echo "2. PostgreSQL"
|
||||||
|
read -p "数据库类型(1/2,默认 1): " DB_CHOICE
|
||||||
|
case "$DB_CHOICE" in
|
||||||
|
2)
|
||||||
|
DB_TYPE="postgres"
|
||||||
|
;;
|
||||||
|
""|1)
|
||||||
|
DB_TYPE="sqlite"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "⚠️ 输入无效,默认使用 SQLite"
|
||||||
|
DB_TYPE="sqlite"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
POSTGRES_DB="flux_panel"
|
||||||
|
POSTGRES_USER="flux_panel"
|
||||||
|
POSTGRES_PASSWORD=$(generate_random)
|
||||||
|
|
||||||
|
if [[ "$DB_TYPE" == "postgres" ]]; then
|
||||||
|
DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable"
|
||||||
|
else
|
||||||
|
DATABASE_URL=""
|
||||||
|
fi
|
||||||
|
|
||||||
# 生成JWT密钥
|
# 生成JWT密钥
|
||||||
JWT_SECRET=$(generate_random)
|
JWT_SECRET=$(generate_random)
|
||||||
}
|
}
|
||||||
@@ -280,10 +392,23 @@ JWT_SECRET=$JWT_SECRET
|
|||||||
FRONTEND_PORT=$FRONTEND_PORT
|
FRONTEND_PORT=$FRONTEND_PORT
|
||||||
BACKEND_PORT=$BACKEND_PORT
|
BACKEND_PORT=$BACKEND_PORT
|
||||||
FLUX_VERSION=$RESOLVED_VERSION
|
FLUX_VERSION=$RESOLVED_VERSION
|
||||||
|
|
||||||
|
DB_TYPE=$DB_TYPE
|
||||||
|
DATABASE_URL=$DATABASE_URL
|
||||||
|
|
||||||
|
POSTGRES_DB=$POSTGRES_DB
|
||||||
|
POSTGRES_USER=$POSTGRES_USER
|
||||||
|
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "🚀 启动 docker 服务..."
|
echo "🚀 启动 docker 服务..."
|
||||||
$DOCKER_CMD up -d
|
if [[ "$DB_TYPE" == "postgres" ]]; then
|
||||||
|
$DOCKER_CMD up -d postgres
|
||||||
|
wait_for_postgres_healthy
|
||||||
|
$DOCKER_CMD up -d backend frontend
|
||||||
|
else
|
||||||
|
$DOCKER_CMD up -d backend frontend
|
||||||
|
fi
|
||||||
|
|
||||||
echo "🎉 部署完成"
|
echo "🎉 部署完成"
|
||||||
echo "🌐 访问地址: http://服务器IP:$FRONTEND_PORT"
|
echo "🌐 访问地址: http://服务器IP:$FRONTEND_PORT"
|
||||||
@@ -300,6 +425,12 @@ update_panel() {
|
|||||||
echo "🔄 开始更新面板..."
|
echo "🔄 开始更新面板..."
|
||||||
check_docker
|
check_docker
|
||||||
|
|
||||||
|
if [[ ! -f ".env" ]]; then
|
||||||
|
echo "⚠️ 未找到 .env,默认按 SQLite 模式更新"
|
||||||
|
fi
|
||||||
|
CURRENT_DB_TYPE=$(get_current_db_type)
|
||||||
|
echo "🗄️ 当前数据库类型:$CURRENT_DB_TYPE"
|
||||||
|
|
||||||
echo "🔍 获取最新版本号..."
|
echo "🔍 获取最新版本号..."
|
||||||
LATEST_VERSION=$(resolve_latest_release_tag) || {
|
LATEST_VERSION=$(resolve_latest_release_tag) || {
|
||||||
echo "❌ 无法获取最新版本号,更新终止"
|
echo "❌ 无法获取最新版本号,更新终止"
|
||||||
@@ -333,49 +464,113 @@ update_panel() {
|
|||||||
$DOCKER_CMD down
|
$DOCKER_CMD down
|
||||||
|
|
||||||
echo "⬇️ 拉取最新镜像..."
|
echo "⬇️ 拉取最新镜像..."
|
||||||
$DOCKER_CMD pull
|
if [[ "$CURRENT_DB_TYPE" == "postgres" ]]; then
|
||||||
|
$DOCKER_CMD pull backend frontend postgres
|
||||||
|
else
|
||||||
|
$DOCKER_CMD pull backend frontend
|
||||||
|
fi
|
||||||
|
|
||||||
echo "🚀 启动更新后的服务..."
|
echo "🚀 启动更新后的服务..."
|
||||||
$DOCKER_CMD up -d
|
if [[ "$CURRENT_DB_TYPE" == "postgres" ]]; then
|
||||||
|
$DOCKER_CMD up -d postgres
|
||||||
|
wait_for_postgres_healthy
|
||||||
|
$DOCKER_CMD up -d backend frontend
|
||||||
|
else
|
||||||
|
$DOCKER_CMD up -d backend frontend
|
||||||
|
fi
|
||||||
|
|
||||||
# 等待服务启动
|
# 等待服务启动
|
||||||
echo "⏳ 等待服务启动..."
|
echo "⏳ 等待服务启动..."
|
||||||
|
|
||||||
# 检查后端容器健康状态
|
if ! wait_for_backend_healthy; then
|
||||||
echo "🔍 检查后端服务状态..."
|
echo "🛑 更新终止"
|
||||||
for i in {1..90}; do
|
return 1
|
||||||
if docker ps --format "{{.Names}}" | grep -q "^flux-panel-backend$"; then
|
fi
|
||||||
BACKEND_HEALTH=$(docker inspect -f '{{.State.Health.Status}}' flux-panel-backend 2>/dev/null || echo "unknown")
|
|
||||||
if [[ "$BACKEND_HEALTH" == "healthy" ]]; then
|
|
||||||
echo "✅ 后端服务健康检查通过"
|
|
||||||
break
|
|
||||||
elif [[ "$BACKEND_HEALTH" == "starting" ]]; then
|
|
||||||
# 继续等待
|
|
||||||
:
|
|
||||||
elif [[ "$BACKEND_HEALTH" == "unhealthy" ]]; then
|
|
||||||
echo "⚠️ 后端健康状态:$BACKEND_HEALTH"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "⚠️ 后端容器未找到或未运行"
|
|
||||||
BACKEND_HEALTH="not_running"
|
|
||||||
fi
|
|
||||||
if [ $i -eq 90 ]; then
|
|
||||||
echo "❌ 后端服务启动超时(90秒)"
|
|
||||||
echo "🔍 当前状态:$(docker inspect -f '{{.State.Health.Status}}' flux-panel-backend 2>/dev/null || echo '容器不存在')"
|
|
||||||
echo "🛑 更新终止"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
# 每15秒显示一次进度
|
|
||||||
if [ $((i % 15)) -eq 1 ]; then
|
|
||||||
echo "⏳ 等待后端服务启动... ($i/90) 状态:${BACKEND_HEALTH:-unknown}"
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "✅ 更新完成"
|
echo "✅ 更新完成"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
migrate_to_postgres() {
|
||||||
|
local current_db_type postgres_db postgres_user postgres_password database_url
|
||||||
|
|
||||||
|
echo "🔄 开始迁移 SQLite -> PostgreSQL..."
|
||||||
|
check_docker
|
||||||
|
|
||||||
|
if [[ ! -f ".env" ]]; then
|
||||||
|
echo "❌ 未找到 .env 文件,请先安装面板"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "docker-compose.yml" ]]; then
|
||||||
|
echo "⚠️ 未找到 docker-compose.yml 文件,正在下载..."
|
||||||
|
DOCKER_COMPOSE_URL=$(get_docker_compose_url)
|
||||||
|
echo "📡 选择配置文件:$(basename "$DOCKER_COMPOSE_URL")"
|
||||||
|
curl -L -o docker-compose.yml "$DOCKER_COMPOSE_URL"
|
||||||
|
echo "✅ docker-compose.yml 下载完成"
|
||||||
|
fi
|
||||||
|
|
||||||
|
current_db_type=$(get_current_db_type)
|
||||||
|
if [[ "$current_db_type" == "postgres" ]]; then
|
||||||
|
echo "ℹ️ 当前已使用 PostgreSQL,无需迁移"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
postgres_db=$(get_env_var "POSTGRES_DB")
|
||||||
|
postgres_user=$(get_env_var "POSTGRES_USER")
|
||||||
|
postgres_password=$(get_env_var "POSTGRES_PASSWORD")
|
||||||
|
|
||||||
|
postgres_db=${postgres_db:-flux_panel}
|
||||||
|
postgres_user=${postgres_user:-flux_panel}
|
||||||
|
postgres_password=${postgres_password:-$(generate_random)}
|
||||||
|
|
||||||
|
upsert_env_var ".env" "POSTGRES_DB" "$postgres_db"
|
||||||
|
upsert_env_var ".env" "POSTGRES_USER" "$postgres_user"
|
||||||
|
upsert_env_var ".env" "POSTGRES_PASSWORD" "$postgres_password"
|
||||||
|
|
||||||
|
echo "🛑 停止当前服务..."
|
||||||
|
docker stop -t 30 flux-panel-backend 2>/dev/null || true
|
||||||
|
docker stop -t 10 vite-frontend 2>/dev/null || true
|
||||||
|
echo "⏳ 等待数据同步..."
|
||||||
|
sleep 5
|
||||||
|
$DOCKER_CMD down
|
||||||
|
|
||||||
|
echo "💾 备份 SQLite 数据到当前目录..."
|
||||||
|
if ! docker run --rm -v sqlite_data:/data -v "$(pwd)":/backup alpine sh -c "cp /data/gost.db /backup/gost.db.bak"; then
|
||||||
|
echo "❌ SQLite 备份失败,迁移终止"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🚀 启动 PostgreSQL..."
|
||||||
|
$DOCKER_CMD up -d postgres
|
||||||
|
if ! wait_for_postgres_healthy; then
|
||||||
|
echo "🛑 PostgreSQL 未就绪,迁移终止"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔄 执行 pgloader 迁移..."
|
||||||
|
if ! docker run --rm --network gost-network -v sqlite_data:/sqlite dimitri/pgloader:latest pgloader /sqlite/gost.db "postgresql://${postgres_user}:${postgres_password}@postgres:5432/${postgres_db}"; then
|
||||||
|
echo "❌ pgloader 迁移失败,迁移终止(如报 28P01,可执行 docker volume rm postgres_data 后重试)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
database_url="postgresql://${postgres_user}:${postgres_password}@postgres:5432/${postgres_db}?sslmode=disable"
|
||||||
|
upsert_env_var ".env" "DB_TYPE" "postgres"
|
||||||
|
upsert_env_var ".env" "DATABASE_URL" "$database_url"
|
||||||
|
|
||||||
|
echo "🚀 启动迁移后的服务..."
|
||||||
|
$DOCKER_CMD up -d postgres backend frontend
|
||||||
|
|
||||||
|
echo "⏳ 等待服务启动..."
|
||||||
|
if ! wait_for_backend_healthy; then
|
||||||
|
echo "🛑 迁移后服务启动失败"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ SQLite -> PostgreSQL 迁移完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 卸载功能
|
# 卸载功能
|
||||||
uninstall_panel() {
|
uninstall_panel() {
|
||||||
@@ -428,6 +623,11 @@ main() {
|
|||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
4)
|
4)
|
||||||
|
migrate_to_postgres
|
||||||
|
delete_self
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
5)
|
||||||
echo "👋 退出脚本"
|
echo "👋 退出脚本"
|
||||||
delete_self
|
delete_self
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
Reference in New Issue
Block a user