Add multi-server data model + legacy DB migration (step 0)

Adds Server.ID/Name, ServerInterface.Name, Client.ServerID, and a new
ServerSetting type as additive fields so existing single-server code
paths keep working unchanged. jsondb.Init() now detects a pre-existing
single-server db/server/ layout and mirrors it into new servers/,
server_settings/, server_hashes/ collections plus backfills ServerID
on existing clients, without touching/removing the legacy files yet.
This commit is contained in:
sysops
2026-07-11 22:35:45 +02:00
parent e5c0a5888f
commit 40d7862702
4 changed files with 142 additions and 0 deletions
+127
View File
@@ -4,9 +4,11 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"log"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/sdomino/scribble"
@@ -17,6 +19,10 @@ import (
"github.com/ngoduykhanh/wireguard-ui/util"
)
// legacyDefaultServerID is the synthetic ID assigned to a pre-existing
// single-server installation when it is migrated to the multi-server layout.
const legacyDefaultServerID = "wg0"
type JsonDB struct {
conn *scribble.Driver
dbPath string
@@ -161,6 +167,11 @@ func (o *JsonDB) Init() error {
}
}
// migrate legacy single-server layout to the new multi-server layout
if err := o.migrateLegacyServer(); err != nil {
return err
}
// init cache
for _, i := range results {
user := model.User{}
@@ -186,6 +197,122 @@ func (o *JsonDB) Init() error {
return nil
}
// migrateLegacyServer detects a pre-multi-server DB layout (single
// "server" collection, no "servers" registry yet) and migrates it into
// the new per-server layout ("servers/<id>.json", "server_settings/<id>.json",
// "server_hashes/<id>.json"), backfilling ServerID on every existing client.
// It is idempotent: it only runs once, guarded by the absence of the
// "servers" directory, and never deletes or modifies the legacy "server"
// collection - existing store methods keep reading/writing it directly
// until they are switched over to the new layout in a later step.
func (o *JsonDB) migrateLegacyServer() error {
legacyServerPath := path.Join(o.dbPath, "server")
legacyInterfacePath := path.Join(legacyServerPath, "interfaces.json")
newServersPath := path.Join(o.dbPath, "servers")
if _, err := os.Stat(legacyInterfacePath); os.IsNotExist(err) {
// nothing to migrate (fresh install)
return nil
}
if _, err := os.Stat(newServersPath); err == nil {
// already migrated
return nil
}
var legacyInterface model.ServerInterface
if err := o.conn.Read("server", "interfaces", &legacyInterface); err != nil {
return fmt.Errorf("migration: cannot read legacy server interface: %v", err)
}
var legacyKeyPair model.ServerKeypair
if err := o.conn.Read("server", "keypair", &legacyKeyPair); err != nil {
return fmt.Errorf("migration: cannot read legacy server keypair: %v", err)
}
var legacyGlobalSettings model.GlobalSetting
if err := o.conn.Read("server", "global_settings", &legacyGlobalSettings); err != nil {
return fmt.Errorf("migration: cannot read legacy global settings: %v", err)
}
var legacyHashes model.ClientServerHashes
hasHashes := true
if err := o.conn.Read("server", "hashes", &legacyHashes); err != nil {
hasHashes = false
}
serverID := legacyDefaultServerID
ifaceName := legacyDefaultServerID
if legacyGlobalSettings.ConfigFilePath != "" {
base := path.Base(legacyGlobalSettings.ConfigFilePath)
if strings.HasSuffix(base, ".conf") {
ifaceName = strings.TrimSuffix(base, ".conf")
}
}
legacyInterface.Name = ifaceName
server := model.Server{
ID: serverID,
Name: "Default Server",
KeyPair: &legacyKeyPair,
Interface: &legacyInterface,
}
if err := o.conn.Write("servers", serverID, server); err != nil {
return fmt.Errorf("migration: cannot write servers/%s.json: %v", serverID, err)
}
if err := util.ManagePerms(path.Join(newServersPath, serverID+".json")); err != nil {
return err
}
serverSetting := model.ServerSetting{
EndpointAddress: legacyGlobalSettings.EndpointAddress,
FirewallMark: legacyGlobalSettings.FirewallMark,
Table: legacyGlobalSettings.Table,
ConfigFilePath: legacyGlobalSettings.ConfigFilePath,
UpdatedAt: time.Now().UTC(),
}
if err := o.conn.Write("server_settings", serverID, serverSetting); err != nil {
return fmt.Errorf("migration: cannot write server_settings/%s.json: %v", serverID, err)
}
if err := util.ManagePerms(path.Join(o.dbPath, "server_settings", serverID+".json")); err != nil {
return err
}
if hasHashes {
if err := o.conn.Write("server_hashes", serverID, legacyHashes); err != nil {
return fmt.Errorf("migration: cannot write server_hashes/%s.json: %v", serverID, err)
}
if err := util.ManagePerms(path.Join(o.dbPath, "server_hashes", serverID+".json")); err != nil {
return err
}
}
// backfill ServerID on every existing client
clientRecords, err := o.conn.ReadAll("clients")
if err != nil && err != scribble.ErrMissingCollection {
return fmt.Errorf("migration: cannot read clients: %v", err)
}
for _, rec := range clientRecords {
var client model.Client
if err := json.Unmarshal(rec, &client); err != nil {
return fmt.Errorf("migration: cannot decode client json: %v", err)
}
if client.ServerID != "" {
continue
}
client.ServerID = serverID
if err := o.conn.Write("clients", client.ID, client); err != nil {
return fmt.Errorf("migration: cannot backfill server_id on client %s: %v", client.ID, err)
}
}
// NOTE: the legacy "server" directory is intentionally left in place
// (not renamed/removed) at this stage. Existing store methods (GetServer,
// GetGlobalSettings, SaveServerInterface, ...) still read/write it
// directly until they are switched over to the new per-server layout;
// removing it now would break the running app. The rename-to-backup
// step happens once those methods are migrated.
log.Printf("Migrated legacy single-server DB to multi-server layout (server id: %s); legacy db/server/ kept in place for now", serverID)
return nil
}
// GetUsers func to get all users from the database
func (o *JsonDB) GetUsers() ([]model.User, error) {
var users []model.User