fix: harden import/export flow

This commit is contained in:
LLLLLLiulei
2026-02-05 18:59:30 +08:00
parent 0b45d48e85
commit 37047919ab
5 changed files with 53 additions and 37 deletions

View File

@@ -21,8 +21,8 @@ const (
) )
type DataPayload struct { type DataPayload struct {
Type string `json:"type"` Type string `json:"type,omitempty"`
Version int `json:"version"` Version int `json:"version,omitempty"`
ExportedAt string `json:"exported_at"` ExportedAt string `json:"exported_at"`
Proxies []DataProxy `json:"proxies"` Proxies []DataProxy `json:"proxies"`
Accounts []DataAccount `json:"accounts"` Accounts []DataAccount `json:"accounts"`
@@ -160,8 +160,6 @@ func (h *AccountHandler) ExportData(c *gin.Context) {
} }
payload := DataPayload{ payload := DataPayload{
Type: dataType,
Version: dataVersion,
ExportedAt: time.Now().UTC().Format(time.RFC3339), ExportedAt: time.Now().UTC().Format(time.RFC3339),
Proxies: dataProxies, Proxies: dataProxies,
Accounts: dataAccounts, Accounts: dataAccounts,
@@ -218,9 +216,17 @@ func (h *AccountHandler) ImportData(c *gin.Context) {
}) })
continue continue
} }
normalizedStatus := normalizeProxyStatus(item.Status)
if existingID, ok := proxyKeyToID[key]; ok { if existingID, ok := proxyKeyToID[key]; ok {
proxyKeyToID[key] = existingID proxyKeyToID[key] = existingID
result.ProxyReused++ result.ProxyReused++
if normalizedStatus != "" {
if proxy, err := h.adminService.GetProxy(c.Request.Context(), existingID); err == nil && proxy != nil && proxy.Status != normalizedStatus {
_, _ = h.adminService.UpdateProxy(c.Request.Context(), existingID, &service.UpdateProxyInput{
Status: normalizedStatus,
})
}
}
continue continue
} }
@@ -245,9 +251,9 @@ func (h *AccountHandler) ImportData(c *gin.Context) {
proxyKeyToID[key] = created.ID proxyKeyToID[key] = created.ID
result.ProxyCreated++ result.ProxyCreated++
if item.Status != "" && item.Status != created.Status { if normalizedStatus != "" && normalizedStatus != created.Status {
_, _ = h.adminService.UpdateProxy(c.Request.Context(), created.ID, &service.UpdateProxyInput{ _, _ = h.adminService.UpdateProxy(c.Request.Context(), created.ID, &service.UpdateProxyInput{
Status: item.Status, Status: normalizedStatus,
}) })
} }
} }
@@ -465,15 +471,18 @@ func parseIncludeProxies(c *gin.Context) (bool, error) {
} }
func validateDataHeader(payload DataPayload) error { func validateDataHeader(payload DataPayload) error {
if payload.Type == "" { if payload.Type != "" && payload.Type != dataType && payload.Type != legacyDataType {
return errors.New("data type is required")
}
if payload.Type != dataType && payload.Type != legacyDataType {
return fmt.Errorf("unsupported data type: %s", payload.Type) return fmt.Errorf("unsupported data type: %s", payload.Type)
} }
if payload.Version != dataVersion { if payload.Version != 0 && payload.Version != dataVersion {
return fmt.Errorf("unsupported data version: %d", payload.Version) return fmt.Errorf("unsupported data version: %d", payload.Version)
} }
if payload.Proxies == nil {
return errors.New("proxies is required")
}
if payload.Accounts == nil {
return errors.New("accounts is required")
}
return nil return nil
} }
@@ -493,9 +502,8 @@ func validateDataProxy(item DataProxy) error {
return fmt.Errorf("proxy protocol is invalid: %s", item.Protocol) return fmt.Errorf("proxy protocol is invalid: %s", item.Protocol)
} }
if item.Status != "" { if item.Status != "" {
switch item.Status { normalizedStatus := normalizeProxyStatus(item.Status)
case service.StatusActive, service.StatusDisabled, "inactive": if normalizedStatus != service.StatusActive && normalizedStatus != "inactive" {
default:
return fmt.Errorf("proxy status is invalid: %s", item.Status) return fmt.Errorf("proxy status is invalid: %s", item.Status)
} }
} }
@@ -538,3 +546,17 @@ func defaultProxyName(name string) string {
} }
return name return name
} }
func normalizeProxyStatus(status string) string {
normalized := strings.TrimSpace(strings.ToLower(status))
switch normalized {
case "":
return ""
case service.StatusActive:
return service.StatusActive
case "inactive", service.StatusDisabled:
return "inactive"
default:
return normalized
}
}

View File

@@ -120,7 +120,8 @@ func TestExportDataIncludesSecrets(t *testing.T) {
var resp dataResponse var resp dataResponse
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
require.Equal(t, 0, resp.Code) require.Equal(t, 0, resp.Code)
require.Equal(t, dataType, resp.Data.Type) require.Empty(t, resp.Data.Type)
require.Equal(t, 0, resp.Data.Version)
require.Len(t, resp.Data.Proxies, 1) require.Len(t, resp.Data.Proxies, 1)
require.Equal(t, "pass", resp.Data.Proxies[0].Password) require.Equal(t, "pass", resp.Data.Proxies[0].Password)
require.Len(t, resp.Data.Accounts, 1) require.Len(t, resp.Data.Accounts, 1)

View File

@@ -61,8 +61,6 @@ func (h *ProxyHandler) ExportData(c *gin.Context) {
} }
payload := DataPayload{ payload := DataPayload{
Type: dataType,
Version: dataVersion,
ExportedAt: time.Now().UTC().Format(time.RFC3339), ExportedAt: time.Now().UTC().Format(time.RFC3339),
Proxies: dataProxies, Proxies: dataProxies,
Accounts: []DataAccount{}, Accounts: []DataAccount{},
@@ -123,10 +121,11 @@ func (h *ProxyHandler) ImportData(c *gin.Context) {
continue continue
} }
normalizedStatus := normalizeProxyStatus(item.Status)
if existing, ok := proxyByKey[key]; ok { if existing, ok := proxyByKey[key]; ok {
result.ProxyReused++ result.ProxyReused++
if item.Status != "" && item.Status != existing.Status { if normalizedStatus != "" && normalizedStatus != existing.Status {
if _, err := h.adminService.UpdateProxy(ctx, existing.ID, &service.UpdateProxyInput{Status: item.Status}); err != nil { if _, err := h.adminService.UpdateProxy(ctx, existing.ID, &service.UpdateProxyInput{Status: normalizedStatus}); err != nil {
result.Errors = append(result.Errors, DataImportError{ result.Errors = append(result.Errors, DataImportError{
Kind: "proxy", Kind: "proxy",
Name: item.Name, Name: item.Name,
@@ -160,8 +159,8 @@ func (h *ProxyHandler) ImportData(c *gin.Context) {
result.ProxyCreated++ result.ProxyCreated++
proxyByKey[key] = *created proxyByKey[key] = *created
if item.Status != "" && item.Status != created.Status { if normalizedStatus != "" && normalizedStatus != created.Status {
if _, err := h.adminService.UpdateProxy(ctx, created.ID, &service.UpdateProxyInput{Status: item.Status}); err != nil { if _, err := h.adminService.UpdateProxy(ctx, created.ID, &service.UpdateProxyInput{Status: normalizedStatus}); err != nil {
result.Errors = append(result.Errors, DataImportError{ result.Errors = append(result.Errors, DataImportError{
Kind: "proxy", Kind: "proxy",
Name: item.Name, Name: item.Name,
@@ -170,7 +169,7 @@ func (h *ProxyHandler) ImportData(c *gin.Context) {
}) })
} }
} }
latencyProbeIDs = append(latencyProbeIDs, created.ID) // CreateProxy already triggers a latency probe, avoid double probing here.
} }
if len(latencyProbeIDs) > 0 { if len(latencyProbeIDs) > 0 {
@@ -186,18 +185,10 @@ func (h *ProxyHandler) ImportData(c *gin.Context) {
} }
func (h *ProxyHandler) getProxiesByIDs(ctx context.Context, ids []int64) ([]service.Proxy, error) { func (h *ProxyHandler) getProxiesByIDs(ctx context.Context, ids []int64) ([]service.Proxy, error) {
out := make([]service.Proxy, 0, len(ids)) if len(ids) == 0 {
for _, id := range ids { return []service.Proxy{}, nil
proxy, err := h.adminService.GetProxy(ctx, id)
if err != nil {
return nil, err
}
if proxy == nil {
continue
}
out = append(out, *proxy)
} }
return out, nil return h.adminService.GetProxiesByIDs(ctx, ids)
} }
func parseProxyIDs(c *gin.Context) ([]int64, error) { func parseProxyIDs(c *gin.Context) ([]int64, error) {

View File

@@ -69,7 +69,8 @@ func TestProxyExportDataRespectsFilters(t *testing.T) {
var resp proxyDataResponse var resp proxyDataResponse
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
require.Equal(t, 0, resp.Code) require.Equal(t, 0, resp.Code)
require.Equal(t, dataType, resp.Data.Type) require.Empty(t, resp.Data.Type)
require.Equal(t, 0, resp.Data.Version)
require.Len(t, resp.Data.Proxies, 1) require.Len(t, resp.Data.Proxies, 1)
require.Len(t, resp.Data.Accounts, 0) require.Len(t, resp.Data.Accounts, 0)
require.Equal(t, "https", resp.Data.Proxies[0].Protocol) require.Equal(t, "https", resp.Data.Proxies[0].Protocol)
@@ -156,6 +157,7 @@ func TestProxyImportDataReusesAndTriggersLatencyProbe(t *testing.T) {
"status": "active", "status": "active",
}, },
}, },
"accounts": []map[string]any{},
}, },
} }
@@ -181,6 +183,6 @@ func TestProxyImportDataReusesAndTriggersLatencyProbe(t *testing.T) {
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
adminSvc.mu.Lock() adminSvc.mu.Lock()
defer adminSvc.mu.Unlock() defer adminSvc.mu.Unlock()
return len(adminSvc.testedProxyIDs) == 2 return len(adminSvc.testedProxyIDs) == 1
}, time.Second, 10*time.Millisecond) }, time.Second, 10*time.Millisecond)
} }

View File

@@ -728,8 +728,8 @@ export interface UpdateProxyRequest {
} }
export interface AdminDataPayload { export interface AdminDataPayload {
type: string type?: string
version: number version?: number
exported_at: string exported_at: string
proxies: AdminDataProxy[] proxies: AdminDataProxy[]
accounts: AdminDataAccount[] accounts: AdminDataAccount[]