chore: 初始提交 Fonu 1.0.2
- 公网反代:Nginx 状态卡片(版本 / 运行状态 / 配置校验 / 端口监听) - 部署设置第一、二梯队(HTTPS 开关、安全头、限流、连接数、仅中国大陆 IP) - 隧道检测兼容 frps v0.68 / v0.71(V2 与旧版端点) - 恢复被清空的 internal/frp/store.go、internal/api/ddns_handler.go、web/src/views/DdnsView.vue
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func Open(path string) (*sql.DB, error) {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("file:%s?_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)", path)
|
||||
conn, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn.SetMaxOpenConns(1)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func Migrate(ctx context.Context, conn *sql.DB, migrationsDir string) error {
|
||||
if err := ensureMigrationsTable(ctx, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(migrationsDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var files []string
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".sql") {
|
||||
continue
|
||||
}
|
||||
files = append(files, e.Name())
|
||||
}
|
||||
sort.Strings(files)
|
||||
|
||||
for _, name := range files {
|
||||
version, err := parseVersion(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
applied, err := isApplied(ctx, conn, version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if applied {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(migrationsDir, name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := conn.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.ExecContext(ctx, string(content)); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return fmt.Errorf("migration %s failed: %w", name, err)
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `INSERT INTO schema_migrations(version) VALUES (?)`, version); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return fmt.Errorf("record migration %s failed: %w", name, err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureMigrationsTable(ctx context.Context, conn *sql.DB) error {
|
||||
_, err := conn.ExecContext(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
func parseVersion(name string) (int, error) {
|
||||
parts := strings.SplitN(name, "_", 2)
|
||||
if len(parts) == 0 {
|
||||
return 0, fmt.Errorf("invalid migration filename: %s", name)
|
||||
}
|
||||
var version int
|
||||
if _, err := fmt.Sscanf(parts[0], "%d", &version); err != nil {
|
||||
return 0, fmt.Errorf("invalid migration version in %s: %w", name, err)
|
||||
}
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func isApplied(ctx context.Context, conn *sql.DB, version int) (bool, error) {
|
||||
var count int
|
||||
err := conn.QueryRowContext(ctx, `SELECT COUNT(1) FROM schema_migrations WHERE version = ?`, version).Scan(&count)
|
||||
return count > 0, err
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
conn, err := Open(filepath.Join(dir, "fonu.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
migrationsDir := filepath.Join("..", "..", "migrations")
|
||||
if err := Migrate(context.Background(), conn, migrationsDir); err != nil {
|
||||
t.Fatalf("migrate failed: %v", err)
|
||||
}
|
||||
if err := Migrate(context.Background(), conn, migrationsDir); err != nil {
|
||||
t.Fatalf("second migrate failed: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user