mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
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)
|
||||
|
||||
`docker-compose-v4.yml` / `docker-compose-v6.yml` 已包含 PostgreSQL 服务。默认仍使用 SQLite,切换到 PostgreSQL 只需要配置环境变量。
|
||||
安装脚本会根据环境自动下载对应的 Compose 配置并保存为 `docker-compose.yml`。默认仍使用 SQLite,切换到 PostgreSQL 只需要配置环境变量。
|
||||
|
||||
1) 在 `docker-compose` 同目录创建或修改 `.env`:
|
||||
|
||||
@@ -61,47 +61,56 @@ POSTGRES_USER=flux_panel
|
||||
POSTGRES_PASSWORD=replace_with_strong_password
|
||||
```
|
||||
|
||||
2) 启动(IPv4/IPv6 二选一):
|
||||
> 📌 使用安装脚本部署时,`POSTGRES_PASSWORD` 会自动随机生成并写入 `.env`。
|
||||
|
||||
2) 启动服务:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose-v4.yml up -d
|
||||
```
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose-v6.yml up -d
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
3) 如果你想继续使用 SQLite,保留 `DB_TYPE=sqlite`(或不设置 `DB_TYPE`)即可。
|
||||
|
||||
#### 从 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 数据:
|
||||
|
||||
```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"
|
||||
```
|
||||
|
||||
2) 仅启动 PostgreSQL:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose-v4.yml up -d postgres
|
||||
docker compose up -d postgres
|
||||
```
|
||||
|
||||
3) 使用 `pgloader` 迁移:
|
||||
|
||||
```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 并启动:
|
||||
|
||||
```bash
|
||||
source .env
|
||||
export DB_TYPE=postgres
|
||||
export DATABASE_URL="postgres://flux_panel:replace_with_strong_password@postgres:5432/flux_panel?sslmode=disable"
|
||||
docker compose -f docker-compose-v4.yml up -d
|
||||
export DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable"
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### 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).
|
||||
- **Added**: `AGENTS.md` (Project documentation).
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@
|
||||
**A**: 面板安装脚本会自动尝试配置 Docker 的 IPv6。如果失败,请手动检查 `/etc/docker/daemon.json` 配置,确保 `ipv6: true` 且分配了正确的 `fixed-cidr-v6` 子网。
|
||||
|
||||
### 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 后数据丢失?
|
||||
**A**:
|
||||
|
||||
@@ -41,6 +41,8 @@ curl -L https://raw.githubusercontent.com/Sagit-chu/flux-panel/main/panel_instal
|
||||
1. 安装面板
|
||||
2. 更新面板
|
||||
3. 卸载面板
|
||||
4. 迁移到 PostgreSQL
|
||||
5. 退出
|
||||
|
||||
---
|
||||
|
||||
|
||||
+30
-14
@@ -42,7 +42,7 @@ FLVX 默认使用 SQLite 作为数据库,同时也内置了对 PostgreSQL 的
|
||||
|
||||
## 三、全新部署(Docker Compose + PostgreSQL)
|
||||
|
||||
`docker-compose-v4.yml` / `docker-compose-v6.yml` 已包含 PostgreSQL 服务,默认使用 SQLite。只需配置环境变量即可切换到 PostgreSQL。
|
||||
安装脚本会根据环境自动下载对应的 Compose 配置并保存为 `docker-compose.yml`。默认使用 SQLite,只需配置环境变量即可切换到 PostgreSQL。
|
||||
|
||||
### 1. 创建 `.env` 文件
|
||||
|
||||
@@ -67,14 +67,8 @@ POSTGRES_PASSWORD=替换为强密码
|
||||
|
||||
### 2. 启动服务
|
||||
|
||||
IPv4 环境:
|
||||
```bash
|
||||
docker compose -f docker-compose-v4.yml up -d
|
||||
```
|
||||
|
||||
IPv6 环境:
|
||||
```bash
|
||||
docker compose -f docker-compose-v6.yml up -d
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 3. 验证
|
||||
@@ -96,11 +90,29 @@ docker logs flux-panel-postgres
|
||||
|
||||
如果你已经在使用 SQLite 并且希望迁移到 PostgreSQL,请按照以下步骤操作。
|
||||
|
||||
### 快速方式:脚本菜单一键迁移(推荐)
|
||||
|
||||
如果你是通过安装脚本部署面板,可直接执行:
|
||||
|
||||
```bash
|
||||
./panel_install.sh
|
||||
# 选择 4. 迁移到 PostgreSQL
|
||||
```
|
||||
|
||||
脚本会自动完成以下操作:
|
||||
- 备份 SQLite 数据到当前目录(`gost.db.bak`)
|
||||
- 启动并等待 PostgreSQL 健康检查通过
|
||||
- 使用 `pgloader` 导入 SQLite 数据
|
||||
- 自动写入 `.env` 的 `DB_TYPE=postgres` 与 `DATABASE_URL`
|
||||
- 重启服务并等待后端健康检查
|
||||
|
||||
### 手动方式:按步骤迁移
|
||||
|
||||
### 1. 备份 SQLite 数据
|
||||
|
||||
```bash
|
||||
# 停止所有服务
|
||||
docker compose -f docker-compose-v4.yml down
|
||||
docker compose down
|
||||
|
||||
# 备份 SQLite 数据文件到当前目录
|
||||
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
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose-v4.yml up -d postgres
|
||||
docker compose up -d postgres
|
||||
```
|
||||
|
||||
等待 PostgreSQL 完全就绪:
|
||||
@@ -127,19 +139,23 @@ docker inspect --format='{{.State.Health.Status}}' flux-panel-postgres
|
||||
### 4. 使用 pgloader 迁移数据
|
||||
|
||||
```bash
|
||||
source .env
|
||||
docker run --rm \
|
||||
--network gost-network \
|
||||
-v sqlite_data:/sqlite \
|
||||
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. 启动全部服务
|
||||
|
||||
```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. 验证迁移
|
||||
@@ -182,7 +198,7 @@ DATABASE_URL=postgres://flux_panel:你的强密码@数据库地址:5432/flux_pan
|
||||
如果使用外部 PostgreSQL,可以在启动时不启动内置的 postgres 服务:
|
||||
|
||||
```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)
|
||||
}
|
||||
}
|
||||
|
||||
if db.Dialect() == store.DialectPostgres {
|
||||
if err := ensurePostgresIDDefaults(db); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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 {
|
||||
if err == nil {
|
||||
return false
|
||||
|
||||
+234
-34
@@ -203,7 +203,8 @@ show_menu() {
|
||||
echo "1. 安装面板"
|
||||
echo "2. 更新面板"
|
||||
echo "3. 卸载面板"
|
||||
echo "4. 退出"
|
||||
echo "4. 迁移到 PostgreSQL"
|
||||
echo "5. 退出"
|
||||
echo "==============================================="
|
||||
}
|
||||
|
||||
@@ -232,6 +233,90 @@ upsert_env_var() {
|
||||
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() {
|
||||
echo ""
|
||||
@@ -253,6 +338,33 @@ get_config_params() {
|
||||
read -p "后端端口(默认 6365): " BACKEND_PORT
|
||||
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_SECRET=$(generate_random)
|
||||
}
|
||||
@@ -280,10 +392,23 @@ JWT_SECRET=$JWT_SECRET
|
||||
FRONTEND_PORT=$FRONTEND_PORT
|
||||
BACKEND_PORT=$BACKEND_PORT
|
||||
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
|
||||
|
||||
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 "🌐 访问地址: http://服务器IP:$FRONTEND_PORT"
|
||||
@@ -300,6 +425,12 @@ update_panel() {
|
||||
echo "🔄 开始更新面板..."
|
||||
check_docker
|
||||
|
||||
if [[ ! -f ".env" ]]; then
|
||||
echo "⚠️ 未找到 .env,默认按 SQLite 模式更新"
|
||||
fi
|
||||
CURRENT_DB_TYPE=$(get_current_db_type)
|
||||
echo "🗄️ 当前数据库类型:$CURRENT_DB_TYPE"
|
||||
|
||||
echo "🔍 获取最新版本号..."
|
||||
LATEST_VERSION=$(resolve_latest_release_tag) || {
|
||||
echo "❌ 无法获取最新版本号,更新终止"
|
||||
@@ -333,49 +464,113 @@ update_panel() {
|
||||
$DOCKER_CMD down
|
||||
|
||||
echo "⬇️ 拉取最新镜像..."
|
||||
$DOCKER_CMD pull
|
||||
if [[ "$CURRENT_DB_TYPE" == "postgres" ]]; then
|
||||
$DOCKER_CMD pull backend frontend postgres
|
||||
else
|
||||
$DOCKER_CMD pull backend frontend
|
||||
fi
|
||||
|
||||
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 "🔍 检查后端服务状态..."
|
||||
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 "✅ 后端服务健康检查通过"
|
||||
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
|
||||
if ! wait_for_backend_healthy; then
|
||||
echo "🛑 更新终止"
|
||||
return 1
|
||||
fi
|
||||
|
||||
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() {
|
||||
@@ -428,6 +623,11 @@ main() {
|
||||
exit 0
|
||||
;;
|
||||
4)
|
||||
migrate_to_postgres
|
||||
delete_self
|
||||
exit 0
|
||||
;;
|
||||
5)
|
||||
echo "👋 退出脚本"
|
||||
delete_self
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user