增加数据库支持
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user