增加数据库支持

This commit is contained in:
2026-06-03 14:09:48 +08:00
parent 44dfb14cf4
commit d887845909
8 changed files with 635 additions and 7 deletions
+256
View File
@@ -0,0 +1,256 @@
package main
import (
"database/sql"
"encoding/json"
"fmt"
"os"
"path/filepath"
_ "github.com/go-sql-driver/mysql"
_ "modernc.org/sqlite"
)
const (
databaseDriverSQLite = "sqlite"
databaseDriverMySQL = "mysql"
)
type store struct {
db *sql.DB
driver string
}
type nodeInfoRecord struct {
NodeID string
NodeNum int64
UserID any
LongName any
ShortName any
HWModel any
Role any
IsLicensed bool
PublicKey any
ContentJSON []byte
}
func openStore(cfg databaseConfig) (*store, error) {
var dsn string
switch cfg.Driver {
case databaseDriverSQLite:
if err := os.MkdirAll(filepath.Dir(cfg.SQLite.Path), 0755); err != nil {
return nil, fmt.Errorf("create sqlite directory %s: %w", filepath.Dir(cfg.SQLite.Path), err)
}
dsn = cfg.SQLite.Path
case databaseDriverMySQL:
dsn = cfg.MySQL.DSN
default:
return nil, fmt.Errorf("unsupported database driver %q", cfg.Driver)
}
db, err := sql.Open(cfg.Driver, dsn)
if err != nil {
return nil, fmt.Errorf("open %s database: %w", cfg.Driver, err)
}
if err := db.Ping(); err != nil {
db.Close()
return nil, fmt.Errorf("ping %s database: %w", cfg.Driver, err)
}
s := &store{db: db, driver: cfg.Driver}
if err := s.migrate(); err != nil {
db.Close()
return nil, err
}
return s, nil
}
func (s *store) Close() error {
if s == nil || s.db == nil {
return nil
}
return s.db.Close()
}
func (s *store) migrate() error {
var query string
switch s.driver {
case databaseDriverSQLite:
query = `CREATE TABLE IF NOT EXISTS nodeinfo (
node_id TEXT PRIMARY KEY,
node_num INTEGER NOT NULL,
user_id TEXT,
long_name TEXT,
short_name TEXT,
hw_model TEXT,
role TEXT,
is_licensed BOOLEAN NOT NULL DEFAULT FALSE,
public_key TEXT,
content_json TEXT NOT NULL,
first_seen_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);`
case databaseDriverMySQL:
query = `CREATE TABLE IF NOT EXISTS nodeinfo (
node_id VARCHAR(32) NOT NULL PRIMARY KEY,
node_num BIGINT UNSIGNED NOT NULL,
user_id VARCHAR(128),
long_name TEXT,
short_name VARCHAR(64),
hw_model VARCHAR(128),
role VARCHAR(128),
is_licensed BOOLEAN NOT NULL DEFAULT FALSE,
public_key TEXT,
content_json JSON NOT NULL,
first_seen_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);`
default:
return fmt.Errorf("unsupported database driver %q", s.driver)
}
if _, err := s.db.Exec(query); err != nil {
return fmt.Errorf("migrate nodeinfo table: %w", err)
}
return nil
}
func (s *store) UpsertNodeInfo(record map[string]any) error {
node, err := nodeInfoFromRecord(record)
if err != nil {
return err
}
var query string
switch s.driver {
case databaseDriverSQLite:
query = `INSERT INTO nodeinfo (
node_id, node_num, user_id, long_name, short_name,
hw_model, role, is_licensed, public_key, content_json,
first_seen_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT(node_id) DO UPDATE SET
node_num = excluded.node_num,
user_id = excluded.user_id,
long_name = excluded.long_name,
short_name = excluded.short_name,
hw_model = excluded.hw_model,
role = excluded.role,
is_licensed = excluded.is_licensed,
public_key = excluded.public_key,
content_json = excluded.content_json,
updated_at = CURRENT_TIMESTAMP;`
case databaseDriverMySQL:
query = `INSERT INTO nodeinfo (
node_id, node_num, user_id, long_name, short_name,
hw_model, role, is_licensed, public_key, content_json,
first_seen_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE
node_num = VALUES(node_num),
user_id = VALUES(user_id),
long_name = VALUES(long_name),
short_name = VALUES(short_name),
hw_model = VALUES(hw_model),
role = VALUES(role),
is_licensed = VALUES(is_licensed),
public_key = VALUES(public_key),
content_json = VALUES(content_json),
updated_at = CURRENT_TIMESTAMP;`
default:
return fmt.Errorf("unsupported database driver %q", s.driver)
}
_, err = s.db.Exec(query,
node.NodeID,
node.NodeNum,
node.UserID,
node.LongName,
node.ShortName,
node.HWModel,
node.Role,
node.IsLicensed,
node.PublicKey,
string(node.ContentJSON),
)
if err != nil {
return fmt.Errorf("upsert nodeinfo %s: %w", node.NodeID, err)
}
return nil
}
func nodeInfoFromRecord(record map[string]any) (*nodeInfoRecord, error) {
if record["type"] != "nodeinfo" {
return nil, fmt.Errorf("record type %v is not nodeinfo", record["type"])
}
nodeID, ok := record["from"].(string)
if !ok || nodeID == "" {
return nil, fmt.Errorf("nodeinfo missing from")
}
nodeNum, err := int64FromAny(record["from_num"])
if err != nil {
return nil, fmt.Errorf("nodeinfo from_num: %w", err)
}
contentJSON, err := json.Marshal(record)
if err != nil {
return nil, fmt.Errorf("encode nodeinfo content_json: %w", err)
}
return &nodeInfoRecord{
NodeID: nodeID,
NodeNum: nodeNum,
UserID: nullableString(record["user_id"]),
LongName: nullableString(record["long_name"]),
ShortName: nullableString(record["short_name"]),
HWModel: nullableString(record["hw_model"]),
Role: nullableString(record["role"]),
IsLicensed: boolFromAny(record["is_licensed"]),
PublicKey: nullableString(record["public_key"]),
ContentJSON: contentJSON,
}, nil
}
func int64FromAny(value any) (int64, error) {
switch v := value.(type) {
case int:
return int64(v), nil
case int8:
return int64(v), nil
case int16:
return int64(v), nil
case int32:
return int64(v), nil
case int64:
return v, nil
case uint:
return int64(v), nil
case uint8:
return int64(v), nil
case uint16:
return int64(v), nil
case uint32:
return int64(v), nil
case uint64:
return int64(v), nil
case float64:
return int64(v), nil
default:
return 0, fmt.Errorf("unsupported value %T", value)
}
}
func nullableString(value any) any {
if value == nil {
return nil
}
s, ok := value.(string)
if !ok || s == "" {
return nil
}
return s
}
func boolFromAny(value any) bool {
b, _ := value.(bool)
return b
}