Skip to content

Commit a82b0b7

Browse files
committed
update:更新导入导出功能到2.0版本
1 parent b34e8bf commit a82b0b7

5 files changed

Lines changed: 320 additions & 96 deletions

File tree

app/endpoints/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ export default function EndpointsPage() {
523523
break;
524524
}}}>
525525
<DropdownItem key="addTunnel" startContent={<FontAwesomeIcon icon={faPlus}/>} className="text-primary" color="primary">添加实例</DropdownItem>
526-
<DropdownItem key="refresTunnel" startContent={<FontAwesomeIcon icon={faRotateRight}/>} className="text-secondary" color="secondary">刷新实例</DropdownItem>
526+
<DropdownItem key="refresTunnel" startContent={<FontAwesomeIcon icon={faSync}/>} className="text-secondary" color="secondary">同步实例</DropdownItem>
527527
<DropdownItem key="rename" startContent={<FontAwesomeIcon icon={faPen} />} className="text-warning" color="warning">重命名</DropdownItem>
528528
<DropdownItem key="copy" startContent={<FontAwesomeIcon icon={faCopy}/>} className="text-success" color="success">复制配置</DropdownItem>
529529
<DropdownItem

cmd/server/main.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ func initDatabase(db *sql.DB) error {
336336
createTunnelTable := `
337337
CREATE TABLE IF NOT EXISTS "Tunnel" (
338338
id INTEGER PRIMARY KEY AUTOINCREMENT,
339-
name TEXT NOT NULL UNIQUE,
339+
name TEXT NOT NULL,
340340
endpointId INTEGER NOT NULL,
341341
mode TEXT NOT NULL,
342342
status TEXT NOT NULL DEFAULT 'stopped',
@@ -357,10 +357,10 @@ func initDatabase(db *sql.DB) error {
357357
udpTx INTEGER DEFAULT 0,
358358
min INTEGER,
359359
max INTEGER,
360+
restart BOOLEAN DEFAULT FALSE,
360361
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
361362
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
362-
lastEventTime DATETIME,
363-
FOREIGN KEY (endpointId) REFERENCES "Endpoint"(id) ON DELETE CASCADE
363+
lastEventTime DATETIME
364364
);`
365365

366366
createTunnelRecycleTable := `
@@ -385,8 +385,7 @@ func initDatabase(db *sql.DB) error {
385385
udpRx INTEGER DEFAULT 0,
386386
udpTx INTEGER DEFAULT 0,
387387
min INTEGER,
388-
max INTEGER,
389-
FOREIGN KEY (endpointId) REFERENCES "Endpoint"(id) ON DELETE CASCADE
388+
max INTEGER
390389
);`
391390

392391
createEndpointSSE := `
@@ -405,8 +404,7 @@ func initDatabase(db *sql.DB) error {
405404
udpRx INTEGER DEFAULT 0,
406405
udpTx INTEGER DEFAULT 0,
407406
logs TEXT,
408-
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
409-
FOREIGN KEY (endpointId) REFERENCES "Endpoint"(id) ON DELETE CASCADE
407+
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
410408
);`
411409

412410
createTunnelLog := `

components/layout/navbar-user.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export const NavbarUser = () => {
205205
const url = window.URL.createObjectURL(blob);
206206
const a = document.createElement('a');
207207
a.href = url;
208-
a.download = `nodepass-data-${new Date().toISOString().split('T')[0]}.json`;
208+
a.download = `nodepassdash-${new Date().toISOString().split('T')[0]}.json`;
209209
document.body.appendChild(a);
210210
a.click();
211211
window.URL.revokeObjectURL(url);
@@ -245,7 +245,7 @@ export const NavbarUser = () => {
245245
if (!selectedFile) {
246246
addToast({
247247
title: "请选择文件",
248-
description: "请先选择要导入的数据文件",
248+
description: "请先选择要导入的端点配置文件",
249249
color: "danger",
250250
});
251251
return;

internal/api/data.go

Lines changed: 66 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package api
33
import (
44
"database/sql"
55
"encoding/json"
6-
"fmt"
76
"net/http"
87
"time"
98

@@ -21,36 +20,13 @@ func NewDataHandler(db *sql.DB, mgr *sse.Manager) *DataHandler {
2120
return &DataHandler{db: db, sseManager: mgr}
2221
}
2322

24-
// EndpointExport 导出端点结构
23+
// EndpointExport 导出端点结构(简化版,仅包含基本配置信息)
2524
type EndpointExport struct {
26-
Name string `json:"name"`
27-
URL string `json:"url"`
28-
APIPath string `json:"apiPath"`
29-
APIKey string `json:"apiKey"`
30-
Status string `json:"status"`
31-
Color string `json:"color,omitempty"`
32-
Tunnels []TunnelExport `json:"tunnels,omitempty"`
33-
}
34-
35-
// TunnelExport 导出隧道结构
36-
type TunnelExport struct {
37-
Name string `json:"name"`
38-
Mode string `json:"mode"`
39-
Status string `json:"status"`
40-
TunnelAddress string `json:"tunnelAddress"`
41-
TunnelPort string `json:"tunnelPort"`
42-
TargetAddress string `json:"targetAddress"`
43-
TargetPort string `json:"targetPort"`
44-
TLSMode string `json:"tlsMode"`
45-
CertPath string `json:"certPath,omitempty"`
46-
KeyPath string `json:"keyPath,omitempty"`
47-
LogLevel string `json:"logLevel"`
48-
CommandLine string `json:"commandLine"`
49-
InstanceID string `json:"instanceId,omitempty"`
50-
TCPRx string `json:"tcpRx,omitempty"`
51-
TCPTx string `json:"tcpTx,omitempty"`
52-
UDPRx string `json:"udpRx,omitempty"`
53-
UDPTx string `json:"udpTx,omitempty"`
25+
Name string `json:"name"`
26+
URL string `json:"url"`
27+
APIPath string `json:"apiPath"`
28+
APIKey string `json:"apiKey"`
29+
Color string `json:"color,omitempty"`
5430
}
5531

5632
// ---------- 导出 ----------
@@ -60,8 +36,8 @@ func (h *DataHandler) HandleExport(w http.ResponseWriter, r *http.Request) {
6036
return
6137
}
6238

63-
// 查询端点
64-
rows, err := h.db.Query(`SELECT id, name, url, apiPath, apiKey, status, color FROM "Endpoint" ORDER BY id`)
39+
// 查询端点(仅导出基本配置信息,不包括状态和隧道信息)
40+
rows, err := h.db.Query(`SELECT name, url, apiPath, apiKey, COALESCE(color, '') as color FROM "Endpoint" ORDER BY id`)
6541
if err != nil {
6642
log.Errorf("export query endpoints: %v", err)
6743
http.Error(w, "export failed", http.StatusInternalServerError)
@@ -71,59 +47,23 @@ func (h *DataHandler) HandleExport(w http.ResponseWriter, r *http.Request) {
7147

7248
var endpoints []EndpointExport
7349
for rows.Next() {
74-
var epID int64
7550
var ep EndpointExport
76-
if err := rows.Scan(&epID, &ep.Name, &ep.URL, &ep.APIPath, &ep.APIKey, &ep.Status, &ep.Color); err != nil {
51+
if err := rows.Scan(&ep.Name, &ep.URL, &ep.APIPath, &ep.APIKey, &ep.Color); err != nil {
7752
continue
7853
}
79-
// 查询该端点隧道
80-
tRows, err := h.db.Query(`SELECT name, mode, status, tunnelAddress, tunnelPort, targetAddress, targetPort, tlsMode, certPath, keyPath, logLevel, commandLine, instanceId, tcpRx, tcpTx, udpRx, udpTx FROM "Tunnel" WHERE endpointId = ?`, epID)
81-
if err == nil {
82-
for tRows.Next() {
83-
var t TunnelExport
84-
var tcpRx, tcpTx, udpRx, udpTx sql.NullInt64
85-
var instanceNS sql.NullString
86-
var certNS, keyNS sql.NullString
87-
if err := tRows.Scan(&t.Name, &t.Mode, &t.Status, &t.TunnelAddress, &t.TunnelPort, &t.TargetAddress, &t.TargetPort, &t.TLSMode, &certNS, &keyNS, &t.LogLevel, &t.CommandLine, &instanceNS, &tcpRx, &tcpTx, &udpRx, &udpTx); err == nil {
88-
if certNS.Valid {
89-
t.CertPath = certNS.String
90-
}
91-
if keyNS.Valid {
92-
t.KeyPath = keyNS.String
93-
}
94-
if instanceNS.Valid {
95-
t.InstanceID = instanceNS.String
96-
}
97-
if tcpRx.Valid {
98-
t.TCPRx = fmt.Sprintf("%d", tcpRx.Int64)
99-
}
100-
if tcpTx.Valid {
101-
t.TCPTx = fmt.Sprintf("%d", tcpTx.Int64)
102-
}
103-
if udpRx.Valid {
104-
t.UDPRx = fmt.Sprintf("%d", udpRx.Int64)
105-
}
106-
if udpTx.Valid {
107-
t.UDPTx = fmt.Sprintf("%d", udpTx.Int64)
108-
}
109-
ep.Tunnels = append(ep.Tunnels, t)
110-
}
111-
}
112-
tRows.Close()
113-
}
11454
endpoints = append(endpoints, ep)
11555
}
11656

11757
payload := map[string]interface{}{
118-
"version": "1.0",
58+
"version": "2.0", // 更新版本号以表示新的简化格式
11959
"timestamp": time.Now().Format(time.RFC3339),
12060
"data": map[string]interface{}{
12161
"endpoints": endpoints,
12262
},
12363
}
12464

12565
w.Header().Set("Content-Type", "application/json")
126-
w.Header().Set("Content-Disposition", "attachment; filename=nodepass-data.json")
66+
w.Header().Set("Content-Disposition", "attachment; filename=nodepass-endpoints.json")
12767
json.NewEncoder(w).Encode(payload)
12868
}
12969

@@ -147,13 +87,23 @@ func (h *DataHandler) HandleImport(w http.ResponseWriter, r *http.Request) {
14787
}
14888

14989
var skippedEndpoints int
150-
var importedTunnels int
90+
var importedEndpoints int
15191

15292
tx, err := h.db.Begin()
15393
if err != nil {
15494
http.Error(w, "db error", http.StatusInternalServerError)
15595
return
15696
}
97+
defer tx.Rollback()
98+
99+
// 存储新创建的端点信息,用于后续启动SSE
100+
var newEndpoints []struct {
101+
ID int64
102+
URL string
103+
APIPath string
104+
APIKey string
105+
}
106+
157107
for _, ep := range importData.Data.Endpoints {
158108
var exists bool
159109
if err := tx.QueryRow(`SELECT EXISTS(SELECT 1 FROM "Endpoint" WHERE url = ? AND apiPath = ?)`, ep.URL, ep.APIPath).Scan(&exists); err != nil {
@@ -163,29 +113,59 @@ func (h *DataHandler) HandleImport(w http.ResponseWriter, r *http.Request) {
163113
skippedEndpoints++
164114
continue
165115
}
166-
res, err := tx.Exec(`INSERT INTO "Endpoint" (name, url, apiPath, apiKey, status, color, tunnelCount, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`, ep.Name, ep.URL, ep.APIPath, ep.APIKey, ep.Status, ep.Color)
116+
117+
// 插入端点,设置默认状态为 OFFLINE
118+
result, err := tx.Exec(`INSERT INTO "Endpoint" (name, url, apiPath, apiKey, status, color, tunnelCount, createdAt, updatedAt) VALUES (?, ?, ?, ?, 'OFFLINE', ?, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
119+
ep.Name, ep.URL, ep.APIPath, ep.APIKey, ep.Color)
167120
if err != nil {
121+
log.Errorf("insert endpoint failed: %v", err)
168122
continue
169123
}
170-
epID, _ := res.LastInsertId()
171-
for _, t := range ep.Tunnels {
172-
_, _ = tx.Exec(`INSERT INTO "Tunnel" (name, mode, status, tunnelAddress, tunnelPort, targetAddress, targetPort, tlsMode, certPath, keyPath, logLevel, commandLine, instanceId, tcpRx, tcpTx, udpRx, udpTx, endpointId, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
173-
t.Name, t.Mode, t.Status, t.TunnelAddress, t.TunnelPort, t.TargetAddress, t.TargetPort, t.TLSMode, t.CertPath, t.KeyPath, t.LogLevel, t.CommandLine, t.InstanceID, t.TCPRx, t.TCPTx, t.UDPRx, t.UDPTx, epID)
174-
importedTunnels++
124+
125+
// 获取新创建的端点ID
126+
endpointID, err := result.LastInsertId()
127+
if err != nil {
128+
log.Errorf("get last insert id failed: %v", err)
129+
continue
175130
}
131+
132+
// 保存端点信息用于后续启动SSE
133+
newEndpoints = append(newEndpoints, struct {
134+
ID int64
135+
URL string
136+
APIPath string
137+
APIKey string
138+
}{
139+
ID: endpointID,
140+
URL: ep.URL,
141+
APIPath: ep.APIPath,
142+
APIKey: ep.APIKey,
143+
})
144+
145+
importedEndpoints++
146+
}
147+
148+
if err := tx.Commit(); err != nil {
149+
http.Error(w, "commit failed", http.StatusInternalServerError)
150+
return
176151
}
177-
tx.Commit()
178152

179-
// 重置 SSE
153+
// 为每个新导入的端点启动SSE监听(学习HandleCreateEndpoint的逻辑)
180154
if h.sseManager != nil {
181-
h.sseManager.Close()
182-
h.sseManager.InitializeSystem()
155+
for _, ep := range newEndpoints {
156+
go func(endpointID int64, url, apiPath, apiKey string) {
157+
log.Infof("[Master-%v] 导入成功,准备启动 SSE 监听", endpointID)
158+
if err := h.sseManager.ConnectEndpoint(endpointID, url, apiPath, apiKey); err != nil {
159+
log.Errorf("[Master-%v] 启动 SSE 监听失败: %v", endpointID, err)
160+
}
161+
}(ep.ID, ep.URL, ep.APIPath, ep.APIKey)
162+
}
183163
}
184164

185165
json.NewEncoder(w).Encode(map[string]interface{}{
186-
"success": true,
187-
"message": "数据导入成功",
188-
"skippedEndpoints": skippedEndpoints,
189-
"tunnels": importedTunnels,
166+
"success": true,
167+
"message": "端点配置导入成功",
168+
"importedEndpoints": importedEndpoints,
169+
"skippedEndpoints": skippedEndpoints,
190170
})
191171
}

0 commit comments

Comments
 (0)