fix(backend): include forward_port in backup export/import

This commit is contained in:
sagit
2026-02-15 01:44:57 +00:00
parent 4e4193e0b0
commit fd5cfc2a40
2 changed files with 232 additions and 13 deletions
+67 -13
View File
@@ -2012,19 +2012,25 @@ type ChainTunnelBackup struct {
}
type ForwardBackup struct {
ID int64 `json:"id"`
UserID int64 `json:"userId"`
UserName string `json:"userName"`
Name string `json:"name"`
TunnelID int64 `json:"tunnelId"`
RemoteAddr string `json:"remoteAddr"`
Strategy string `json:"strategy"`
InFlow int64 `json:"inFlow"`
OutFlow int64 `json:"outFlow"`
CreatedTime int64 `json:"createdTime"`
UpdatedTime int64 `json:"updatedTime"`
Status int `json:"status"`
Inx int `json:"inx"`
ID int64 `json:"id"`
UserID int64 `json:"userId"`
UserName string `json:"userName"`
Name string `json:"name"`
TunnelID int64 `json:"tunnelId"`
RemoteAddr string `json:"remoteAddr"`
Strategy string `json:"strategy"`
InFlow int64 `json:"inFlow"`
OutFlow int64 `json:"outFlow"`
CreatedTime int64 `json:"createdTime"`
UpdatedTime int64 `json:"updatedTime"`
Status int `json:"status"`
Inx int `json:"inx"`
ForwardPorts *[]ForwardPortBackup `json:"forwardPorts,omitempty"`
}
type ForwardPortBackup struct {
NodeID int64 `json:"nodeId"`
Port int `json:"port"`
}
type UserTunnelBackup struct {
@@ -2426,11 +2432,47 @@ func (r *Repository) exportForwards() ([]ForwardBackup, error) {
if inx.Valid {
f.Inx = int(inx.Int64)
}
forwardPorts, err := r.exportForwardPorts(f.ID)
if err != nil {
return nil, err
}
portsCopy := append([]ForwardPortBackup(nil), forwardPorts...)
f.ForwardPorts = &portsCopy
forwards = append(forwards, f)
}
return forwards, rows.Err()
}
func (r *Repository) exportForwardPorts(forwardID int64) ([]ForwardPortBackup, error) {
rows, err := r.db.Query(`
SELECT node_id, port
FROM forward_port
WHERE forward_id = ?
ORDER BY id ASC
`, forwardID)
if err != nil {
return nil, err
}
defer rows.Close()
ports := make([]ForwardPortBackup, 0)
for rows.Next() {
var fp ForwardPortBackup
if err := rows.Scan(&fp.NodeID, &fp.Port); err != nil {
return nil, err
}
ports = append(ports, fp)
}
if err := rows.Err(); err != nil {
return nil, err
}
return ports, nil
}
func (r *Repository) exportUserTunnels() ([]UserTunnelBackup, error) {
rows, err := r.db.Query(`
SELECT id, user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status
@@ -2845,6 +2887,18 @@ func (r *Repository) importForwards(db Execer, forwards []ForwardBackup, now int
if err != nil {
return count, err
}
if f.ForwardPorts != nil {
if _, err := db.Exec(`DELETE FROM forward_port WHERE forward_id = ?`, f.ID); err != nil {
return count, err
}
for _, fp := range *f.ForwardPorts {
if _, err := db.Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, f.ID, fp.NodeID, fp.Port); err != nil {
return count, err
}
}
}
count++
}
return count, nil
@@ -311,6 +311,171 @@ func TestBackupExportImportRestoreContracts(t *testing.T) {
}
})
t.Run("backup export and import preserve forward ports", func(t *testing.T) {
now := time.Now().UnixMilli()
tunnelRes, err := repo.DB().Exec(`
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, "backup-forward-tunnel", 1.0, 1, "tls", 0, now, now, 1, "", 88)
if err != nil {
t.Fatalf("seed tunnel for forward backup: %v", err)
}
tunnelID, err := tunnelRes.LastInsertId()
if err != nil {
t.Fatalf("read tunnel id for forward backup: %v", err)
}
forwardRes, err := repo.DB().Exec(`
INSERT INTO forward(user_id, user_name, name, tunnel_id, remote_addr, strategy, in_flow, out_flow, created_time, updated_time, status, inx)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, 1, "admin_user", "backup-forward", tunnelID, "127.0.0.1:9000", "fifo", 0, 0, now, now, 1, 88)
if err != nil {
t.Fatalf("seed forward for backup: %v", err)
}
forwardID, err := forwardRes.LastInsertId()
if err != nil {
t.Fatalf("read forward id for backup: %v", err)
}
expected := map[int64]int{
2001: 21001,
2002: 21002,
}
for nodeID, port := range expected {
if _, err := repo.DB().Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, forwardID, nodeID, port); err != nil {
t.Fatalf("seed forward_port %d:%d: %v", nodeID, port, err)
}
}
exportReq := httptest.NewRequest(http.MethodPost, "/api/v1/backup/export", bytes.NewBufferString(`{"types":["forwards"]}`))
exportReq.Header.Set("Authorization", adminToken)
exportReq.Header.Set("Content-Type", "application/json")
exportResp := httptest.NewRecorder()
router.ServeHTTP(exportResp, exportReq)
if exportResp.Code != http.StatusOK {
t.Fatalf("expected export status 200, got %d", exportResp.Code)
}
exportBody, err := io.ReadAll(exportResp.Body)
if err != nil {
t.Fatalf("read forwards backup body: %v", err)
}
var payload map[string]interface{}
if err := json.Unmarshal(exportBody, &payload); err != nil {
t.Fatalf("decode forwards backup payload: %v", err)
}
version, _ := payload["version"].(string)
if strings.TrimSpace(version) == "" {
t.Fatalf("expected backup payload version, body=%s", string(exportBody))
}
forwardsRaw, ok := payload["forwards"].([]interface{})
if !ok {
t.Fatalf("expected forwards array in payload, body=%s", string(exportBody))
}
foundForward := false
foundPorts := map[int64]int{}
for _, item := range forwardsRaw {
forwardMap, ok := item.(map[string]interface{})
if !ok {
continue
}
idValue, ok := forwardMap["id"].(float64)
if !ok || int64(idValue) != forwardID {
continue
}
foundForward = true
portsRaw, ok := forwardMap["forwardPorts"].([]interface{})
if !ok {
t.Fatalf("expected forwardPorts for forward %d in payload", forwardID)
}
for _, p := range portsRaw {
portMap, ok := p.(map[string]interface{})
if !ok {
continue
}
nodeID, nodeOK := portMap["nodeId"].(float64)
port, portOK := portMap["port"].(float64)
if nodeOK && portOK {
foundPorts[int64(nodeID)] = int(port)
}
}
break
}
if !foundForward {
t.Fatalf("expected forward %d in exported forwards payload", forwardID)
}
if len(foundPorts) != len(expected) {
t.Fatalf("expected %d exported forward ports, got %d", len(expected), len(foundPorts))
}
for nodeID, port := range expected {
if got, ok := foundPorts[nodeID]; !ok || got != port {
t.Fatalf("expected exported forward port node=%d port=%d, got %v", nodeID, port, foundPorts)
}
}
if _, err := repo.DB().Exec(`DELETE FROM forward_port WHERE forward_id = ?`, forwardID); err != nil {
t.Fatalf("clear forward_port before import: %v", err)
}
if _, err := repo.DB().Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, forwardID, 9999, 39999); err != nil {
t.Fatalf("seed wrong forward_port before import: %v", err)
}
payload["types"] = []string{"forwards"}
importBody, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal forwards import payload: %v", err)
}
importReq := httptest.NewRequest(http.MethodPost, "/api/v1/backup/import", bytes.NewReader(importBody))
importReq.Header.Set("Authorization", adminToken)
importReq.Header.Set("Content-Type", "application/json")
importResp := httptest.NewRecorder()
router.ServeHTTP(importResp, importReq)
var out response.R
if err := json.NewDecoder(importResp.Body).Decode(&out); err != nil {
t.Fatalf("decode forwards import response: %v", err)
}
if out.Code != 0 {
t.Fatalf("expected forwards import code 0, got %d (%s)", out.Code, out.Msg)
}
rows, err := repo.DB().Query(`SELECT node_id, port FROM forward_port WHERE forward_id = ? ORDER BY id ASC`, forwardID)
if err != nil {
t.Fatalf("query forward ports after import: %v", err)
}
defer rows.Close()
after := make(map[int64]int)
for rows.Next() {
var nodeID int64
var port int
if err := rows.Scan(&nodeID, &port); err != nil {
t.Fatalf("scan forward_port row: %v", err)
}
after[nodeID] = port
}
if err := rows.Err(); err != nil {
t.Fatalf("iterate forward_port rows: %v", err)
}
if len(after) != len(expected) {
t.Fatalf("expected %d forward ports after import, got %d (%v)", len(expected), len(after), after)
}
for nodeID, port := range expected {
if got, ok := after[nodeID]; !ok || got != port {
t.Fatalf("expected forward_port node=%d port=%d after import, got %v", nodeID, port, after)
}
}
})
t.Run("backup export tolerates nullable legacy tunnel chain fields", func(t *testing.T) {
now := time.Now().UnixMilli()
res, err := repo.DB().Exec(`