init
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
// Package db manages the SQLite database connection, schema migrations,
|
||||
// and data access for server profiles and connection logs.
|
||||
//
|
||||
// It uses modernc.org/sqlite (a pure-Go driver) so the binary has no
|
||||
// CGO dependency and cross-compiles trivially.
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"lmvpn/internal/paths"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// Store wraps the database handle and provides data access methods.
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// Open creates or opens the SQLite database and runs migrations.
|
||||
func Open() (*Store, error) {
|
||||
db, err := sql.Open("sqlite", paths.DBPath()+"?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open db: %w", err)
|
||||
}
|
||||
db.SetMaxOpenConns(1) // sqlite serialises writes
|
||||
s := &Store{db: db}
|
||||
if err := s.migrate(); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("migrate: %w", err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Close closes the database connection.
|
||||
func (s *Store) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
|
||||
func (s *Store) migrate() error {
|
||||
_, err := s.db.Exec(schema)
|
||||
return err
|
||||
}
|
||||
|
||||
const schema = `
|
||||
CREATE TABLE IF NOT EXISTS server_profiles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
server_url TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
auth_mode TEXT NOT NULL DEFAULT 'both',
|
||||
routing_mode TEXT NOT NULL DEFAULT 'full',
|
||||
custom_cidrs TEXT NOT NULL DEFAULT '',
|
||||
mtu_override INTEGER NOT NULL DEFAULT 0,
|
||||
auto_connect INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
last_connected_at DATETIME
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS connection_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
profile_id INTEGER NOT NULL,
|
||||
started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ended_at DATETIME,
|
||||
assigned_ip TEXT NOT NULL DEFAULT '',
|
||||
rx_bytes INTEGER NOT NULL DEFAULT 0,
|
||||
tx_bytes INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'connected',
|
||||
error_msg TEXT NOT NULL DEFAULT '',
|
||||
FOREIGN KEY (profile_id) REFERENCES server_profiles(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_logs_profile ON connection_logs(profile_id);
|
||||
`
|
||||
@@ -0,0 +1,64 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"lmvpn/internal/model"
|
||||
)
|
||||
|
||||
// StartLog creates a connection log entry and returns its ID.
|
||||
func (s *Store) StartLog(profileID int64) (int64, error) {
|
||||
res, err := s.db.Exec(
|
||||
`INSERT INTO connection_logs (profile_id, started_at, status)
|
||||
VALUES (?, ?, 'connected')`,
|
||||
profileID, time.Now())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("start log: %w", err)
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// FinishLog finalises a connection log entry with final stats.
|
||||
func (s *Store) FinishLog(id int64, status model.ConnectionStatus, assignedIP string, rxBytes, txBytes int64, errMsg string) error {
|
||||
_, err := s.db.Exec(
|
||||
`UPDATE connection_logs SET
|
||||
ended_at = ?, assigned_ip = ?, rx_bytes = ?, tx_bytes = ?,
|
||||
status = ?, error_msg = ?
|
||||
WHERE id = ?`,
|
||||
time.Now(), assignedIP, rxBytes, txBytes, status, errMsg, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finish log %d: %w", id, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecentLogs returns the most recent N connection logs for a profile.
|
||||
func (s *Store) RecentLogs(profileID int64, limit int) ([]model.ConnectionLog, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id, profile_id, started_at, ended_at, assigned_ip,
|
||||
rx_bytes, tx_bytes, status, error_msg
|
||||
FROM connection_logs WHERE profile_id = ?
|
||||
ORDER BY started_at DESC LIMIT ?`,
|
||||
profileID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("recent logs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []model.ConnectionLog
|
||||
for rows.Next() {
|
||||
var l model.ConnectionLog
|
||||
var ended interface{}
|
||||
if err := rows.Scan(&l.ID, &l.ProfileID, &l.StartedAt, &ended,
|
||||
&l.AssignedIP, &l.RxBytes, &l.TxBytes, &l.Status, &l.ErrorMsg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t, ok := ended.(time.Time); ok {
|
||||
l.EndedAt = &t
|
||||
}
|
||||
out = append(out, l)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"lmvpn/internal/model"
|
||||
)
|
||||
|
||||
// CreateProfile inserts a new server profile and returns its ID.
|
||||
func (s *Store) CreateProfile(p *model.ServerProfile) (int64, error) {
|
||||
res, err := s.db.Exec(
|
||||
`INSERT INTO server_profiles
|
||||
(name, server_url, username, auth_mode, routing_mode,
|
||||
custom_cidrs, mtu_override, auto_connect)
|
||||
VALUES (?,?,?,?,?,?,?,?)`,
|
||||
p.Name, p.ServerURL, p.Username, p.AuthMode, p.RoutingMode,
|
||||
p.CustomCIDRs, p.MTUOverride, p.AutoConnect,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("insert profile: %w", err)
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
p.ID = id
|
||||
p.CreatedAt = time.Now()
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// GetProfile returns a single profile by ID.
|
||||
func (s *Store) GetProfile(id int64) (*model.ServerProfile, error) {
|
||||
p := &model.ServerProfile{}
|
||||
var last sql.NullTime
|
||||
err := s.db.QueryRow(
|
||||
`SELECT id, name, server_url, username, auth_mode, routing_mode,
|
||||
custom_cidrs, mtu_override, auto_connect, created_at, last_connected_at
|
||||
FROM server_profiles WHERE id = ?`, id,
|
||||
).Scan(&p.ID, &p.Name, &p.ServerURL, &p.Username, &p.AuthMode,
|
||||
&p.RoutingMode, &p.CustomCIDRs, &p.MTUOverride, &p.AutoConnect,
|
||||
&p.CreatedAt, &last)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get profile %d: %w", id, err)
|
||||
}
|
||||
if last.Valid {
|
||||
p.LastConnectedAt = &last.Time
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// ListProfiles returns all saved profiles ordered by name.
|
||||
func (s *Store) ListProfiles() ([]model.ServerProfile, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id, name, server_url, username, auth_mode, routing_mode,
|
||||
custom_cidrs, mtu_override, auto_connect, created_at, last_connected_at
|
||||
FROM server_profiles ORDER BY name`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list profiles: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []model.ServerProfile
|
||||
for rows.Next() {
|
||||
var p model.ServerProfile
|
||||
var last sql.NullTime
|
||||
if err := rows.Scan(&p.ID, &p.Name, &p.ServerURL, &p.Username,
|
||||
&p.AuthMode, &p.RoutingMode, &p.CustomCIDRs, &p.MTUOverride,
|
||||
&p.AutoConnect, &p.CreatedAt, &last); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if last.Valid {
|
||||
p.LastConnectedAt = &last.Time
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// UpdateProfile updates an existing profile.
|
||||
func (s *Store) UpdateProfile(p *model.ServerProfile) error {
|
||||
_, err := s.db.Exec(
|
||||
`UPDATE server_profiles SET
|
||||
name = ?, server_url = ?, username = ?, auth_mode = ?,
|
||||
routing_mode = ?, custom_cidrs = ?, mtu_override = ?, auto_connect = ?
|
||||
WHERE id = ?`,
|
||||
p.Name, p.ServerURL, p.Username, p.AuthMode,
|
||||
p.RoutingMode, p.CustomCIDRs, p.MTUOverride, p.AutoConnect, p.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update profile %d: %w", p.ID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteProfile removes a profile by ID.
|
||||
func (s *Store) DeleteProfile(id int64) error {
|
||||
_, err := s.db.Exec(`DELETE FROM server_profiles WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete profile %d: %w", id, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TouchLastConnected records the connection timestamp for a profile.
|
||||
func (s *Store) TouchLastConnected(id int64) error {
|
||||
_, err := s.db.Exec(
|
||||
`UPDATE server_profiles SET last_connected_at = ? WHERE id = ?`,
|
||||
time.Now(), id)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user