Files
cnbugs 19c0b190ea Fix connection logs: add background status.log sync task
ConnLogs were never populated because AppendConnLog was never called.
Added StartStatusSync() goroutine that runs every 10 seconds:

1. Iterates all running instances
2. Reads status.log via ParseStatus (status-version 3)
3. For each CLIENT_LIST entry:
   - If no active ConnLog exists → AppendConnLog (new connection)
   - If active ConnLog exists → UpdateActiveConnStats (traffic)
4. For active ConnLogs with no matching CLIENT_LIST → CloseActiveConn

Store additions:
  ListActiveConns(instanceID)  — returns all unclosed connections
  UpdateActiveConnStats()      — updates bytes in/out without flush

StartStatusSync is called from main.go right after Service creation.
UpdateActiveConnStats deliberately skips flush() to avoid writing
db.json every 10 seconds; stats are persisted on disconnect.
2026-08-09 23:52:17 +08:00

426 lines
9.6 KiB
Go

package store
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"time"
"openvpn-manager/internal/model"
)
// Store 简易 JSON 文件存储:
// 适合中小规模运维工具,无需引入数据库。所有变更通过 RWMutex 保护。
type Store struct {
mu sync.RWMutex
path string
data Data
writeCh chan struct{}
}
type Data struct {
Instances []model.Instance `json:"instances"`
Users []model.VPNUser `json:"users"`
Audits []model.AuditLog `json:"audits"`
ConnLogs []model.ConnectionLog `json:"conn_logs"`
Backups []model.Backup `json:"backups"`
Admins []model.AdminUser `json:"admins"`
}
func Open(path string) (*Store, error) {
s := &Store{path: path}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return nil, err
}
if _, err := os.Stat(path); os.IsNotExist(err) {
s.data = Data{}
if err := s.flush(); err != nil {
return nil, err
}
return s, nil
}
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
if len(b) == 0 {
s.data = Data{}
return s, nil
}
if err := json.Unmarshal(b, &s.data); err != nil {
return nil, fmt.Errorf("parse db: %w", err)
}
return s, nil
}
func (s *Store) flush() error {
b, err := json.MarshalIndent(s.data, "", " ")
if err != nil {
return err
}
tmp := s.path + ".tmp"
if err := os.WriteFile(tmp, b, 0o600); err != nil {
return err
}
return os.Rename(tmp, s.path)
}
// ---- Instances ----
func (s *Store) ListInstances() []model.Instance {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]model.Instance, len(s.data.Instances))
copy(out, s.data.Instances)
return out
}
func (s *Store) GetInstance(id string) (*model.Instance, error) {
s.mu.RLock()
defer s.mu.RUnlock()
for i := range s.data.Instances {
if s.data.Instances[i].ID == id {
in := s.data.Instances[i]
return &in, nil
}
}
return nil, fmt.Errorf("instance %s not found", id)
}
func (s *Store) GetInstanceByName(name string) (*model.Instance, error) {
s.mu.RLock()
defer s.mu.RUnlock()
for i := range s.data.Instances {
if s.data.Instances[i].Name == name {
in := s.data.Instances[i]
return &in, nil
}
}
return nil, fmt.Errorf("instance %s not found", name)
}
func (s *Store) UpsertInstance(in model.Instance) error {
s.mu.Lock()
defer s.mu.Unlock()
for i := range s.data.Instances {
if s.data.Instances[i].ID == in.ID {
s.data.Instances[i] = in
return s.flush()
}
}
s.data.Instances = append(s.data.Instances, in)
return s.flush()
}
func (s *Store) DeleteInstance(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
idx := -1
for i := range s.data.Instances {
if s.data.Instances[i].ID == id {
idx = i
break
}
}
if idx < 0 {
return fmt.Errorf("instance %s not found", id)
}
s.data.Instances = append(s.data.Instances[:idx], s.data.Instances[idx+1:]...)
// 同步删除其用户
users := s.data.Users[:0]
for _, u := range s.data.Users {
if u.InstanceID != id {
users = append(users, u)
}
}
s.data.Users = users
return s.flush()
}
// ---- Users ----
func (s *Store) ListUsers(instanceID string) []model.VPNUser {
s.mu.RLock()
defer s.mu.RUnlock()
out := []model.VPNUser{}
for _, u := range s.data.Users {
if instanceID == "" || u.InstanceID == instanceID {
u.PasswordHash = "" // 不向外暴露 hash
out = append(out, u)
}
}
return out
}
func (s *Store) GetUser(id string) (*model.VPNUser, error) {
s.mu.RLock()
defer s.mu.RUnlock()
for i := range s.data.Users {
if s.data.Users[i].ID == id {
u := s.data.Users[i]
return &u, nil
}
}
return nil, fmt.Errorf("user %s not found", id)
}
func (s *Store) GetUserByCN(instanceID, cn string) (*model.VPNUser, error) {
s.mu.RLock()
defer s.mu.RUnlock()
for i := range s.data.Users {
if s.data.Users[i].InstanceID == instanceID && s.data.Users[i].Username == cn {
u := s.data.Users[i]
return &u, nil
}
}
return nil, fmt.Errorf("user %s/%s not found", instanceID, cn)
}
func (s *Store) UpsertUser(u model.VPNUser) error {
s.mu.Lock()
defer s.mu.Unlock()
for i := range s.data.Users {
if s.data.Users[i].ID == u.ID {
s.data.Users[i] = u
return s.flush()
}
}
s.data.Users = append(s.data.Users, u)
return s.flush()
}
func (s *Store) DeleteUser(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
idx := -1
for i := range s.data.Users {
if s.data.Users[i].ID == id {
idx = i
break
}
}
if idx < 0 {
return fmt.Errorf("user %s not found", id)
}
s.data.Users = append(s.data.Users[:idx], s.data.Users[idx+1:]...)
return s.flush()
}
// ---- Audit ----
func (s *Store) AppendAudit(a model.AuditLog) error {
s.mu.Lock()
defer s.mu.Unlock()
s.data.Audits = append(s.data.Audits, a)
// 仅保留最近 5000 条
if len(s.data.Audits) > 5000 {
s.data.Audits = s.data.Audits[len(s.data.Audits)-5000:]
}
return s.flush()
}
func (s *Store) ListAudits(limit int) []model.AuditLog {
s.mu.RLock()
defer s.mu.RUnlock()
if limit <= 0 || limit > len(s.data.Audits) {
limit = len(s.data.Audits)
}
out := make([]model.AuditLog, limit)
copy(out, s.data.Audits[len(s.data.Audits)-limit:])
// 倒序
for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
out[i], out[j] = out[j], out[i]
}
return out
}
// ---- Connection Logs ----
func (s *Store) AppendConnLog(c model.ConnectionLog) error {
s.mu.Lock()
defer s.mu.Unlock()
s.data.ConnLogs = append(s.data.ConnLogs, c)
if len(s.data.ConnLogs) > 20000 {
s.data.ConnLogs = s.data.ConnLogs[len(s.data.ConnLogs)-20000:]
}
return s.flush()
}
func (s *Store) ListConnLogs(instanceID string, limit int) []model.ConnectionLog {
s.mu.RLock()
defer s.mu.RUnlock()
out := []model.ConnectionLog{}
for i := len(s.data.ConnLogs) - 1; i >= 0 && len(out) < limit; i-- {
c := s.data.ConnLogs[i]
if instanceID == "" || c.InstanceID == instanceID {
out = append(out, c)
}
}
return out
}
func (s *Store) FindActiveConn(instanceID, commonName string) *model.ConnectionLog {
s.mu.RLock()
defer s.mu.RUnlock()
for i := len(s.data.ConnLogs) - 1; i >= 0; i-- {
c := s.data.ConnLogs[i]
if c.InstanceID == instanceID && c.CommonName == commonName && c.DisconnectedAt == nil {
cc := c
return &cc
}
}
return nil
}
func (s *Store) CloseActiveConn(instanceID, commonName string, at time.Time) error {
s.mu.Lock()
defer s.mu.Unlock()
for i := len(s.data.ConnLogs) - 1; i >= 0; i-- {
c := &s.data.ConnLogs[i]
if c.InstanceID == instanceID && c.CommonName == commonName && c.DisconnectedAt == nil {
c.DisconnectedAt = &at
return s.flush()
}
}
return nil
}
// ListActiveConns 返回指定实例所有未断开的连接。
func (s *Store) ListActiveConns(instanceID string) []model.ConnectionLog {
s.mu.RLock()
defer s.mu.RUnlock()
var out []model.ConnectionLog
for i := len(s.data.ConnLogs) - 1; i >= 0; i-- {
c := s.data.ConnLogs[i]
if c.InstanceID == instanceID && c.DisconnectedAt == nil {
out = append(out, c)
}
}
return out
}
// UpdateActiveConnStats 更新活跃连接的流量统计。
func (s *Store) UpdateActiveConnStats(instanceID, commonName string, bytesIn, bytesOut int64) error {
s.mu.Lock()
defer s.mu.Unlock()
for i := len(s.data.ConnLogs) - 1; i >= 0; i-- {
c := &s.data.ConnLogs[i]
if c.InstanceID == instanceID && c.CommonName == commonName && c.DisconnectedAt == nil {
c.BytesIn = bytesIn
c.BytesOut = bytesOut
return nil // 不 flush,避免高频写盘
}
}
return nil
}
// ---- Backups ----
func (s *Store) ListBackups() []model.Backup {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]model.Backup, len(s.data.Backups))
copy(out, s.data.Backups)
return out
}
func (s *Store) AddBackup(b model.Backup) error {
s.mu.Lock()
defer s.mu.Unlock()
s.data.Backups = append(s.data.Backups, b)
return s.flush()
}
func (s *Store) DeleteBackup(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
idx := -1
for i := range s.data.Backups {
if s.data.Backups[i].ID == id {
idx = i
break
}
}
if idx < 0 {
return fmt.Errorf("backup %s not found", id)
}
s.data.Backups = append(s.data.Backups[:idx], s.data.Backups[idx+1:]...)
return s.flush()
}
// ---- Admins ----
func (s *Store) ListAdmins() []model.AdminUser {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]model.AdminUser, len(s.data.Admins))
copy(out, s.data.Admins)
// 不返回哈希,调用方负责清空 PasswordHash
for i := range out {
out[i].PasswordHash = ""
}
return out
}
func (s *Store) GetAdmin(id string) (*model.AdminUser, error) {
s.mu.RLock()
defer s.mu.RUnlock()
for i := range s.data.Admins {
if s.data.Admins[i].ID == id {
a := s.data.Admins[i]
return &a, nil
}
}
return nil, fmt.Errorf("admin %s not found", id)
}
// GetAdminByUsername 包含哈希的内部副本。仅供登录校验使用。
func (s *Store) GetAdminByUsername(username string) (*model.AdminUser, error) {
s.mu.RLock()
defer s.mu.RUnlock()
for i := range s.data.Admins {
if s.data.Admins[i].Username == username {
a := s.data.Admins[i]
return &a, nil
}
}
return nil, fmt.Errorf("admin %s not found", username)
}
func (s *Store) UpsertAdmin(a model.AdminUser) error {
s.mu.Lock()
defer s.mu.Unlock()
for i := range s.data.Admins {
if s.data.Admins[i].ID == a.ID {
s.data.Admins[i] = a
return s.flush()
}
}
s.data.Admins = append(s.data.Admins, a)
return s.flush()
}
func (s *Store) DeleteAdmin(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
idx := -1
for i := range s.data.Admins {
if s.data.Admins[i].ID == id {
idx = i
break
}
}
if idx < 0 {
return fmt.Errorf("admin %s not found", id)
}
s.data.Admins = append(s.data.Admins[:idx], s.data.Admins[idx+1:]...)
return s.flush()
}
func (s *Store) AdminCount() int {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.data.Admins)
}