x
", keys["OriginErrorPageHTML"].CurrentValue) } diff --git a/internal/apps/openflare/config_version/snapshot.go b/internal/apps/openflare/config_version/snapshot.go index 9a86b867..093ad477 100644 --- a/internal/apps/openflare/config_version/snapshot.go +++ b/internal/apps/openflare/config_version/snapshot.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "sort" "strconv" "strings" @@ -199,9 +200,7 @@ func buildCurrentConfigBundle(ctx context.Context, requireRoutes bool) (*configB return nil, err } - mainConfig := "" - routeConfig := "" - checksum := "" + var mainConfig, routeConfig, checksum string supportFiles := []SupportFile(nil) rendered, renderErr := renderSnapshotConfig(string(snapshotJSON), certificateFiles) @@ -429,7 +428,7 @@ func buildSnapshotWAFIPGroups(ctx context.Context, idSet map[uint]struct{}) ([]s for id := range idSet { ids = append(ids, id) } - sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) + slices.Sort(ids) groups, err := listWAFIPGroupsByIDs(ctx, ids) if err != nil { return nil, err @@ -477,7 +476,7 @@ func decodeIPList(raw string) ([]string, error) { } var items []string if err := json.Unmarshal([]byte(text), &items); err != nil { - return nil, fmt.Errorf("ip_list payload is invalid") + return nil, errors.New("ip_list payload is invalid") } return items, nil } @@ -629,7 +628,7 @@ func buildCertificateSupportFiles(ctx context.Context, routes []snapshotRoute) ( for certID := range certIDSet { certIDs = append(certIDs, certID) } - sort.Slice(certIDs, func(i, j int) bool { return certIDs[i] < certIDs[j] }) + slices.Sort(certIDs) files := make([]SupportFile, 0, len(certIDs)*supportFilesPerCertificate) for _, certID := range certIDs { certificate, err := repository.GetTLSCertificateByID(ctx, certID) diff --git a/internal/apps/openflare/dashboard/logics_test.go b/internal/apps/openflare/dashboard/logics_test.go index 1f43bf01..970405e1 100644 --- a/internal/apps/openflare/dashboard/logics_test.go +++ b/internal/apps/openflare/dashboard/logics_test.go @@ -20,6 +20,7 @@ import ( ) func setupDashboardTestDB(t *testing.T) func() { + t.Helper() sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ DisableForeignKeyConstraintWhenMigrating: true, }) @@ -124,8 +125,8 @@ func TestGetOverviewStructure(t *testing.T) { onlineNodeCheck := overview.Nodes require.NotEmpty(t, onlineNodeCheck) - assert.Equal(t, 55.0, overview.Capacity.AverageCPUUsagePercent) - assert.Equal(t, 50.0, overview.Capacity.AverageMemoryUsagePercent) + assert.InDelta(t, 55.0, overview.Capacity.AverageCPUUsagePercent, 1e-9) + assert.InDelta(t, 50.0, overview.Capacity.AverageMemoryUsagePercent, 1e-9) assert.Equal(t, 0, overview.Capacity.HighCPUNodes) assert.Equal(t, 0, overview.Capacity.HighMemoryNodes) assert.Equal(t, 0, overview.Capacity.HighStorageNodes) @@ -171,11 +172,11 @@ func TestGetOverviewStructure(t *testing.T) { assert.Equal(t, "online", onlineNode[6]) assert.Equal(t, "healthy", onlineNode[7]) // Latest-per-node health fields (indexes match compressDashboardNodes). - assert.Equal(t, 55.0, onlineNode[11]) // cpu_usage_percent from latest snapshot - assert.Equal(t, 50.0, onlineNode[12]) // memory_usage_percent - assert.Equal(t, int64(12), onlineNode[14]) // request_count from access logs - assert.Equal(t, int64(1), onlineNode[15]) // error_count - assert.Equal(t, int64(4), onlineNode[16]) // unique visitors + assert.InDelta(t, 55.0, onlineNode[11], 1e-9) // cpu_usage_percent from latest snapshot + assert.InDelta(t, 50.0, onlineNode[12], 1e-9) // memory_usage_percent + assert.Equal(t, int64(12), onlineNode[14]) // request_count from access logs + assert.Equal(t, int64(1), onlineNode[15]) // error_count + assert.Equal(t, int64(4), onlineNode[16]) // unique visitors pendingNode := nodeByID["node-dashboard-2"] require.NotNil(t, pendingNode) @@ -183,6 +184,6 @@ func TestGetOverviewStructure(t *testing.T) { assert.Equal(t, "pending", pendingNode[6]) assert.Equal(t, "unknown", pendingNode[7]) - assert.Equal(t, 55.0, overview.Capacity.AverageCPUUsagePercent) + assert.InDelta(t, 55.0, overview.Capacity.AverageCPUUsagePercent, 1e-9) assert.Equal(t, 1, overview.Traffic.ReportedNodes) } diff --git a/internal/apps/openflare/flared/logics.go b/internal/apps/openflare/flared/logics.go index c20ecf26..3e7c3a87 100644 --- a/internal/apps/openflare/flared/logics.go +++ b/internal/apps/openflare/flared/logics.go @@ -28,7 +28,7 @@ const ( // Heartbeat processes an OpenFlared heartbeat and returns runtime settings. func Heartbeat(ctx context.Context, node *model.OpenFlareNode, payload HeartbeatPayload) (*HeartbeatResponse, error) { if node == nil { - return nil, fmt.Errorf("tunnel client node is nil") + return nil, errors.New("tunnel client node is nil") } if node.NodeType != "tunnel_client" { return nil, fmt.Errorf("node %s is not a tunnel_client", node.NodeID) @@ -95,7 +95,7 @@ func Heartbeat(ctx context.Context, node *model.OpenFlareNode, payload Heartbeat // GetTunnelConfig builds the full tunnel routing config for an OpenFlared client. func GetTunnelConfig(ctx context.Context, node *model.OpenFlareNode) (*TunnelConfigResponse, error) { if node == nil { - return nil, fmt.Errorf("node is nil") + return nil, errors.New("node is nil") } activeVersion, err := getActiveConfigMeta(ctx) diff --git a/internal/apps/openflare/flared/routers.go b/internal/apps/openflare/flared/routers.go index 8e5c81da..05f460ac 100644 --- a/internal/apps/openflare/flared/routers.go +++ b/internal/apps/openflare/flared/routers.go @@ -37,7 +37,11 @@ func PostHeartbeat(c *gin.Context) { response.AbortUnauthorized(c, errTunnelTokenInvalid) return } - node := authNode.(*model.OpenFlareNode) + node, ok := authNode.(*model.OpenFlareNode) + if !ok { + response.AbortUnauthorized(c, errTunnelTokenInvalid) + return + } result, err := Heartbeat(c.Request.Context(), node, payload) if apiutil.AbortBadRequestOnError(c, err) { @@ -63,7 +67,11 @@ func GetActiveConfig(c *gin.Context) { response.AbortUnauthorized(c, errTunnelTokenInvalid) return } - node := authNode.(*model.OpenFlareNode) + node, ok := authNode.(*model.OpenFlareNode) + if !ok { + response.AbortUnauthorized(c, errTunnelTokenInvalid) + return + } config, err := GetTunnelConfig(c.Request.Context(), node) if apiutil.AbortBadRequestOnError(c, err) { @@ -91,7 +99,9 @@ func PostApplyLog(c *gin.Context) { return } if authNode, ok := c.Get(ctxFlaredNodeKey); ok { - payload.NodeID = authNode.(*model.OpenFlareNode).NodeID + if node, ok := authNode.(*model.OpenFlareNode); ok { + payload.NodeID = node.NodeID + } } log, err := ReportApplyLog(c.Request.Context(), payload) @@ -115,6 +125,10 @@ func GetWebSocket(c *gin.Context) { response.AbortUnauthorized(c, errTunnelTokenInvalid) return } - node := authNode.(*model.OpenFlareNode) + node, ok := authNode.(*model.OpenFlareNode) + if !ok { + response.AbortUnauthorized(c, errTunnelTokenInvalid) + return + } ofws.ServeFlared(c, node.NodeID) } diff --git a/internal/apps/openflare/geoip/data/data.go b/internal/apps/openflare/geoip/data/data.go index dfbcae06..3d699cba 100644 --- a/internal/apps/openflare/geoip/data/data.go +++ b/internal/apps/openflare/geoip/data/data.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package data embeds the MaxMind GeoLite2 Country database for the control plane. // // Server keeps a Country-only embed so MaxMind provider can seed without network. diff --git a/internal/apps/openflare/geoip/data/data_test.go b/internal/apps/openflare/geoip/data/data_test.go index 8c43a70f..36b99d83 100644 --- a/internal/apps/openflare/geoip/data/data_test.go +++ b/internal/apps/openflare/geoip/data/data_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package data import ( diff --git a/internal/apps/openflare/geoip/runtime.go b/internal/apps/openflare/geoip/runtime.go index d6bac910..42f019ea 100644 --- a/internal/apps/openflare/geoip/runtime.go +++ b/internal/apps/openflare/geoip/runtime.go @@ -26,7 +26,7 @@ const ( var ( runtimeOnce sync.Once - runtimeInitErr error + errRuntimeInit error currentProviderMu sync.RWMutex currentProvider string ) @@ -34,9 +34,9 @@ var ( // EnsureRuntimeProvider loads GeoIP provider config from SystemConfig. func EnsureRuntimeProvider(ctx context.Context) error { runtimeOnce.Do(func() { - runtimeInitErr = applyProviderFromSystemConfig(ctx) + errRuntimeInit = applyProviderFromSystemConfig(ctx) }) - return runtimeInitErr + return errRuntimeInit } // RefreshRuntimeProvider reapplies GeoIPProvider after config updates. @@ -91,11 +91,12 @@ func ensureServerMMDB() (string, error) { if err != nil { return "", err } - if _, err := os.Stat(path); err == nil { + _, statErr := os.Stat(path) + if statErr == nil { return path, nil } - if err != nil && !os.IsNotExist(err) { - return "", err + if !os.IsNotExist(statErr) { + return "", statErr } // Control plane: seed Country from embedded asset (no City; Agent uses disk/image). @@ -115,7 +116,7 @@ func ensureServerMMDB() (string, error) { // ResetRuntimeForTest clears lazy-init state for unit tests. func ResetRuntimeForTest() { runtimeOnce = sync.Once{} - runtimeInitErr = nil + errRuntimeInit = nil currentProviderMu.Lock() currentProvider = "" currentProviderMu.Unlock() diff --git a/internal/apps/openflare/geoip/runtime_test.go b/internal/apps/openflare/geoip/runtime_test.go index 130ae2c9..0b66f181 100644 --- a/internal/apps/openflare/geoip/runtime_test.go +++ b/internal/apps/openflare/geoip/runtime_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package geoip import ( diff --git a/internal/apps/openflare/integration/core_chain_test.go b/internal/apps/openflare/integration/core_chain_test.go index 99cc8e01..8c3f1cb1 100644 --- a/internal/apps/openflare/integration/core_chain_test.go +++ b/internal/apps/openflare/integration/core_chain_test.go @@ -171,7 +171,7 @@ func TestCoreChainMigrationFlow(t *testing.T) { zoneDomains := data["zone_domains"].([]any) assert.Len(t, zoneDomains, 1) assert.Equal(t, "core-chain.example.com", zoneDomains[0].(map[string]any)["domain"]) - assert.Equal(t, float64(originID), data["origin_id"]) + assert.InDelta(t, float64(originID), data["origin_id"], 1e-9) assert.Equal(t, "http://origin.core-chain.internal:8080", data["origin_url"]) }) @@ -257,7 +257,7 @@ func TestCoreChainMigrationFlow(t *testing.T) { listResp := requireAPIOK(t, listRec) listData := unmarshalAPIMap(t, listResp.Data) - assert.Equal(t, float64(1), listData["total"]) + assert.InDelta(t, float64(1), listData["total"], 1e-9) rows, ok := listData["rows"].([]any) require.True(t, ok) @@ -278,10 +278,10 @@ func TestCoreChainMigrationFlow(t *testing.T) { require.Len(t, nodes, 1) nodeView, ok := nodes[0].(map[string]any) require.True(t, ok) - assert.Equal(t, float64(nodeID), nodeView["id"]) + assert.InDelta(t, float64(nodeID), nodeView["id"], 1e-9) assert.Equal(t, nodePublicID, nodeView["node_id"]) assert.Equal(t, "success", nodeView["latest_apply_result"]) assert.Equal(t, configChecksum, nodeView["latest_apply_checksum"]) - assert.Equal(t, float64(2), nodeView["latest_support_file_count"]) + assert.InDelta(t, float64(2), nodeView["latest_support_file_count"], 1e-9) }) } diff --git a/internal/apps/openflare/integration/security_test.go b/internal/apps/openflare/integration/security_test.go index b81d8617..6f9ad370 100644 --- a/internal/apps/openflare/integration/security_test.go +++ b/internal/apps/openflare/integration/security_test.go @@ -119,7 +119,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { assert.NotZero(t, ruleGroupID) assert.Equal(t, "edge-security", data["name"]) assert.Equal(t, false, data["is_global"]) - assert.Equal(t, float64(1), data["revision"]) + assert.InDelta(t, float64(1), data["revision"], 1e-9) assert.NotNil(t, data["graph"]) }) @@ -161,7 +161,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { resp := requireAPIOK(t, rec) data := unmarshalAPIMap(t, resp.Data) - assert.Equal(t, float64(ruleGroupID), data["id"]) + assert.InDelta(t, float64(ruleGroupID), data["id"], 1e-9) assert.Equal(t, "edge-security", data["name"]) }) @@ -244,12 +244,12 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { resp := requireAPIOK(t, rec) data := unmarshalAPIMap(t, resp.Data) - assert.Equal(t, float64(proxyRouteID), data["route_id"]) + assert.InDelta(t, float64(proxyRouteID), data["route_id"], 1e-9) appliedIDs, ok := data["applied_ids"].([]any) require.True(t, ok) require.Len(t, appliedIDs, 1) - assert.Equal(t, float64(ruleGroupID), appliedIDs[0]) + assert.InDelta(t, float64(ruleGroupID), appliedIDs[0], 1e-9) }) t.Run("verify site rule groups binding", func(t *testing.T) { @@ -272,7 +272,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { require.Len(t, appliedGroups, 1) group, ok := appliedGroups[0].(map[string]any) require.True(t, ok) - assert.Equal(t, float64(ruleGroupID), group["id"]) + assert.InDelta(t, float64(ruleGroupID), group["id"], 1e-9) }) t.Run("create TLS certificate with PEM", func(t *testing.T) { @@ -319,7 +319,7 @@ func TestSecurityWAFTLSMigrationFlow(t *testing.T) { domainID = uint(data["id"].(float64)) assert.NotZero(t, domainID) assert.Equal(t, "security.example.com", data["domain"]) - assert.Equal(t, float64(certID), data["cert_id"]) + assert.InDelta(t, float64(certID), data["cert_id"], 1e-9) }) t.Run("create DNS account", func(t *testing.T) { diff --git a/internal/apps/openflare/node/helpers.go b/internal/apps/openflare/node/helpers.go index ef4e3199..e0e7f114 100644 --- a/internal/apps/openflare/node/helpers.go +++ b/internal/apps/openflare/node/helpers.go @@ -8,6 +8,7 @@ import ( "crypto/rand" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "net" @@ -308,6 +309,8 @@ func fetchLatestGitHubRelease(ctx context.Context, repo string, channel releaseC switch normalizeReleaseChannel(string(channel)) { case releaseChannelPreview: return fetchLatestPreviewGitHubRelease(ctx, repo) + case releaseChannelStable: + return fetchLatestStableGitHubRelease(ctx, repo) default: return fetchLatestStableGitHubRelease(ctx, repo) } @@ -317,11 +320,11 @@ func fetchLatestStableGitHubRelease(ctx context.Context, repo string) (*githubRe url := fmt.Sprintf(githubReleasesAPIBase+"/latest", strings.TrimSpace(repo)) req, err := newGitHubReleaseRequest(ctx, url) if err != nil { - return nil, fmt.Errorf("创建更新请求失败") + return nil, errors.New("创建更新请求失败") } resp, err := releaseHTTPClient.Do(req) if err != nil { - return nil, fmt.Errorf("获取最新版本失败: %v", err) + return nil, fmt.Errorf("获取最新版本失败: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -334,11 +337,11 @@ func fetchLatestPreviewGitHubRelease(ctx context.Context, repo string) (*githubR url := fmt.Sprintf(githubReleasesAPIBase+"?per_page=20", strings.TrimSpace(repo)) req, err := newGitHubReleaseRequest(ctx, url) if err != nil { - return nil, fmt.Errorf("创建更新请求失败") + return nil, errors.New("创建更新请求失败") } resp, err := releaseHTTPClient.Do(req) if err != nil { - return nil, fmt.Errorf("获取 preview 版本失败: %v", err) + return nil, fmt.Errorf("获取 preview 版本失败: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -346,7 +349,7 @@ func fetchLatestPreviewGitHubRelease(ctx context.Context, repo string) (*githubR } var releases []githubReleaseResponse if err = json.NewDecoder(resp.Body).Decode(&releases); err != nil { - return nil, fmt.Errorf("解析 preview 版本信息失败") + return nil, errors.New("解析 preview 版本信息失败") } for _, release := range releases { if release.Draft || !release.Prerelease { @@ -355,22 +358,22 @@ func fetchLatestPreviewGitHubRelease(ctx context.Context, repo string) (*githubR releaseCopy := release return &releaseCopy, nil } - return nil, fmt.Errorf("当前没有可用的 preview 发布") + return nil, errors.New("当前没有可用的 preview 发布") } func fetchGitHubReleaseByTag(ctx context.Context, repo string, tag string) (*githubReleaseResponse, error) { tag = strings.TrimSpace(tag) if tag == "" { - return nil, fmt.Errorf("缺少发布版本号") + return nil, errors.New("缺少发布版本号") } url := fmt.Sprintf(githubReleasesAPIBase+"/tags/%s", strings.TrimSpace(repo), tag) req, err := newGitHubReleaseRequest(ctx, url) if err != nil { - return nil, fmt.Errorf("创建更新请求失败") + return nil, errors.New("创建更新请求失败") } resp, err := releaseHTTPClient.Do(req) if err != nil { - return nil, fmt.Errorf("获取指定版本失败: %v", err) + return nil, fmt.Errorf("获取指定版本失败: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode == http.StatusNotFound { @@ -395,7 +398,7 @@ func newGitHubReleaseRequest(ctx context.Context, url string) (*http.Request, er func decodeGitHubRelease(reader io.Reader) (*githubReleaseResponse, error) { var release githubReleaseResponse if err := json.NewDecoder(reader).Decode(&release); err != nil { - return nil, fmt.Errorf("解析版本信息失败") + return nil, errors.New("解析版本信息失败") } return &release, nil } diff --git a/internal/apps/openflare/node/logics.go b/internal/apps/openflare/node/logics.go index b5b4468b..eb11f5f8 100644 --- a/internal/apps/openflare/node/logics.go +++ b/internal/apps/openflare/node/logics.go @@ -361,7 +361,7 @@ func RequestForceSync(ctx context.Context, id uint) (*View, error) { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("无法获取当前激活的配置版本:%s", errNoActiveConfigVersion) } - return nil, fmt.Errorf("无法获取当前激活的配置版本:%v", err) + return nil, fmt.Errorf("无法获取当前激活的配置版本:%w", err) } if !ofws.SendForceSyncConfig(node.NodeID, forceSyncConfigPayload{ Version: activeConfig.Version, @@ -414,14 +414,14 @@ type forceSyncConfigPayload struct { func ValidateDiscoveryToken(ctx context.Context, token string) error { token = strings.TrimSpace(token) if token == "" { - return fmt.Errorf("缺少 Discovery Token") + return errors.New("缺少 Discovery Token") } discoveryToken, err := ensureGlobalDiscoveryToken(ctx) if err != nil { return err } if token != discoveryToken { - return fmt.Errorf("discovery Token 无效") // error 消息首字母小写 + return errors.New("discovery Token 无效") // error 消息首字母小写 } return nil } diff --git a/internal/apps/openflare/observability/access_log_ip_summary_test.go b/internal/apps/openflare/observability/access_log_ip_summary_test.go index 15c14c5f..b14d519d 100644 --- a/internal/apps/openflare/observability/access_log_ip_summary_test.go +++ b/internal/apps/openflare/observability/access_log_ip_summary_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package observability import ( diff --git a/internal/apps/openflare/observability/access_log_logics.go b/internal/apps/openflare/observability/access_log_logics.go index 2d2aa0e6..be9b2c8e 100644 --- a/internal/apps/openflare/observability/access_log_logics.go +++ b/internal/apps/openflare/observability/access_log_logics.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package observability provides monitoring, metrics, and access log analysis for OpenFlare. package observability @@ -175,7 +178,7 @@ type AccessLogIPSummaryList struct { TotalIP int64 `json:"total_ip"` Hours int `json:"hours"` Since time.Time `json:"since"` - Until time.Time `json:"until,omitempty"` + Until time.Time `json:"until,omitzero"` SortBy string `json:"sort_by"` SortOrder string `json:"sort_order"` } diff --git a/internal/apps/openflare/option/logics.go b/internal/apps/openflare/option/logics.go index 9442c932..3795c02a 100644 --- a/internal/apps/openflare/option/logics.go +++ b/internal/apps/openflare/option/logics.go @@ -108,7 +108,7 @@ func updateOptions(ctx context.Context, options []model.OpenFlareOption) error { return nil } -func getStatus(ctx context.Context, baseAPIPath string) (*statusView, error) { +func getStatus(ctx context.Context, baseAPIPath string) *statusView { authSources, err := publicAuthSources(ctx, baseAPIPath) if err != nil { authSources = []publicAuthSourceView{} @@ -128,7 +128,7 @@ func getStatus(ctx context.Context, baseAPIPath string) (*statusView, error) { PasswordRegisterEnabled: passwordRegisterEnabled, CapLoginEnabled: capLoginEnabled, AuthSources: authSources, - }, nil + } } func publicAuthSources(ctx context.Context, baseAPIPath string) ([]publicAuthSourceView, error) { diff --git a/internal/apps/openflare/option/openresty_validators.go b/internal/apps/openflare/option/openresty_validators.go index dddcd475..d4b703ff 100644 --- a/internal/apps/openflare/option/openresty_validators.go +++ b/internal/apps/openflare/option/openresty_validators.go @@ -189,7 +189,7 @@ func validateOpenRestyCacheUseStale(key, trimmed string) error { "http_500": {}, "http_502": {}, "http_503": {}, "http_504": {}, "http_403": {}, "http_404": {}, "http_429": {}, "off": {}, } - for _, token := range strings.Fields(trimmed) { + for token := range strings.FieldsSeq(trimmed) { if _, ok := allowedTokens[token]; !ok { return fmt.Errorf("%s 包含不支持的值 %q", key, token) } @@ -239,7 +239,7 @@ func validateOriginErrorPageStatusCodes(key, trimmed string) error { } codes, err := openrestyrender.ExpandStatusCodeTags(tags) if err != nil { - return fmt.Errorf("%s: %v", key, err) + return fmt.Errorf("%s: %w", key, err) } if len(codes) == 0 { return fmt.Errorf("%s 展开后不能为空", key) diff --git a/internal/apps/openflare/option/routers.go b/internal/apps/openflare/option/routers.go index e7f94bca..349e7a96 100644 --- a/internal/apps/openflare/option/routers.go +++ b/internal/apps/openflare/option/routers.go @@ -22,10 +22,7 @@ import ( // @Failure 500 {object} response.Any "内部错误" // @Router /api/v1/d/status [get] func GetStatusHandler(c *gin.Context) { - view, err := getStatus(c.Request.Context(), "/api/v1/d") - if apiutil.AbortBadRequestOnError(c, err) { - return - } + view := getStatus(c.Request.Context(), "/api/v1/d") c.JSON(http.StatusOK, response.OK(view)) } diff --git a/internal/apps/openflare/option/validate.go b/internal/apps/openflare/option/validate.go index 986a31e7..f48c2b15 100644 --- a/internal/apps/openflare/option/validate.go +++ b/internal/apps/openflare/option/validate.go @@ -176,16 +176,16 @@ func validateUptimeKumaEnabled(ctx context.Context, key, trimmed string, state m username := strings.TrimSpace(state[model.ConfigKeyUptimeKumaUsername]) password := strings.TrimSpace(state[model.ConfigKeyUptimeKumaPassword]) if url == "" { - return fmt.Errorf("启用 Uptime Kuma 时地址不能为空") + return errors.New("启用 Uptime Kuma 时地址不能为空") } if username == "" { - return fmt.Errorf("启用 Uptime Kuma 时用户名不能为空") + return errors.New("启用 Uptime Kuma 时用户名不能为空") } // 如果待验证的密码为空,且当前配置中也没有密码,则报错 if password == "" { existingPwd, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyUptimeKumaPassword) if strings.TrimSpace(existingPwd.Value) == "" { - return fmt.Errorf("启用 Uptime Kuma 时密码不能为空") + return errors.New("启用 Uptime Kuma 时密码不能为空") } } return nil @@ -193,21 +193,21 @@ func validateUptimeKumaEnabled(ctx context.Context, key, trimmed string, state m func validateUptimeKumaUsername(trimmed string, state map[string]string) error { if trimmed == "" && state[model.ConfigKeyUptimeKumaEnabled] == optionValueTrue { - return fmt.Errorf("启用 Uptime Kuma 时用户名不能为空") + return errors.New("启用 Uptime Kuma 时用户名不能为空") } return nil } func validateUptimeKumaURL(trimmed string) error { if trimmed != "" && !strings.HasPrefix(trimmed, "http://") && !strings.HasPrefix(trimmed, "https://") { - return fmt.Errorf("uptime Kuma 地址必须以 http:// 或 https:// 开头") + return errors.New("uptime Kuma 地址必须以 http:// 或 https:// 开头") } return nil } func validateUptimeKumaMonitorScope(trimmed string) error { if trimmed != "all" && trimmed != "selected" { - return fmt.Errorf("监控范围必须为全部站点 (all) 或选择站点 (selected)") + return errors.New("监控范围必须为全部站点 (all) 或选择站点 (selected)") } return nil } diff --git a/internal/apps/openflare/origin/helpers.go b/internal/apps/openflare/origin/helpers.go index 7638da96..c7f6cae5 100644 --- a/internal/apps/openflare/origin/helpers.go +++ b/internal/apps/openflare/origin/helpers.go @@ -34,8 +34,8 @@ func validateOriginAddress(address string) error { if len(address) > maxOriginHostnameLength { return errors.New(errOriginAddressInvalid) } - labels := strings.Split(address, ".") - for _, label := range labels { + labels := strings.SplitSeq(address, ".") + for label := range labels { if len(label) == 0 || len(label) > 63 { return errors.New(errOriginAddressInvalid) } diff --git a/internal/apps/openflare/pages/helpers.go b/internal/apps/openflare/pages/helpers.go index 8e017ea2..2cd66190 100644 --- a/internal/apps/openflare/pages/helpers.go +++ b/internal/apps/openflare/pages/helpers.go @@ -80,18 +80,11 @@ func resolvePagesLimits(ctx context.Context) pagesLimits { historyCount := defaultPagesMaxHistoryCount if value, err := repository.GetIntByKey(ctx, model.ConfigKeyPagesMaxHistoryCount); err == nil { - if value < 0 { - historyCount = 0 - } else { - historyCount = value - } + historyCount = max(value, 0) } packageBytes := int64(packageMB) * bytesPerMiB - extractedBytes := packageBytes * pagesExtractedSizeMultiplier - if extractedBytes < pagesMinExtractedSizeBytes { - extractedBytes = pagesMinExtractedSizeBytes - } + extractedBytes := max(packageBytes*pagesExtractedSizeMultiplier, pagesMinExtractedSizeBytes) return pagesLimits{ PackageBytes: packageBytes, @@ -154,7 +147,7 @@ func normalizePagesFallbackPath(raw string) (string, error) { return "", errors.New("spa fallback 回退路径不能包含空白或控制字符") } } - for _, segment := range strings.Split(value, "/") { + for segment := range strings.SplitSeq(value, "/") { if segment == "." || segment == ".." { return "", errors.New("spa fallback 回退路径不能包含 . 或 .. 路径段") } @@ -233,6 +226,8 @@ func safeTempSuffix(format pagesarchive.Format) string { return "7z" case pagesarchive.FormatTar: return "tar" + case pagesarchive.FormatZip: + return "zip" default: return "zip" } diff --git a/internal/apps/openflare/pages/logics.go b/internal/apps/openflare/pages/logics.go index 007aeeaa..57cdf7f7 100644 --- a/internal/apps/openflare/pages/logics.go +++ b/internal/apps/openflare/pages/logics.go @@ -513,7 +513,7 @@ func pruneProjectDeploymentHistory(ctx context.Context, projectID uint, keepCoun // Two passes: first pass after upload, second pass heals a concurrent race // that inserted another deployment between our list and delete. var lastErr error - for pass := 0; pass < 2; pass++ { + for range 2 { deleted, err := pruneProjectDeploymentHistoryOnce(ctx, projectID, keepCount, preserveCandidateID) if err != nil { lastErr = err diff --git a/internal/apps/openflare/pages/rebind.go b/internal/apps/openflare/pages/rebind.go index b55bb3d3..333894d9 100644 --- a/internal/apps/openflare/pages/rebind.go +++ b/internal/apps/openflare/pages/rebind.go @@ -6,6 +6,7 @@ package pages import ( "context" "encoding/json" + "errors" "fmt" "path" "strings" @@ -89,11 +90,11 @@ func rebindPagesRouteMaps(ctx context.Context, routes []map[string]json.RawMessa if route == nil { continue } - upstreamType, _ := rawJSONString(route["upstream_type"]) + upstreamType := rawJSONString(route["upstream_type"]) if !strings.EqualFold(strings.TrimSpace(upstreamType), "pages") { continue } - siteName, _ := rawJSONString(route["site_name"]) + siteName := rawJSONString(route["site_name"]) projectID, err := resolveProjectIDFromRouteMap(route) if err != nil { if siteName == "" { @@ -151,7 +152,7 @@ func resolveProjectIDFromRouteMap(route map[string]json.RawMessage) (uint, error return deployment.ProjectID, nil } } - return 0, fmt.Errorf("pages 配置无效: 缺少 pages_project_id") + return 0, errors.New("pages 配置无效: 缺少 pages_project_id") } func loadActivePagesProject(ctx context.Context, projectID uint, siteName string) (*model.PagesProject, *model.PagesDeployment, error) { @@ -224,15 +225,15 @@ func buildLivePagesDeployment( }, nil } -func rawJSONString(raw json.RawMessage) (string, bool) { +func rawJSONString(raw json.RawMessage) string { if !isPresentJSON(raw) { - return "", false + return "" } var value string if err := json.Unmarshal(raw, &value); err != nil { - return "", false + return "" } - return value, true + return value } func putJSON(route map[string]json.RawMessage, key string, value any) error { @@ -245,5 +246,5 @@ func putJSON(route map[string]json.RawMessage, key string, value any) error { } func errorsIsNotFound(err error) bool { - return err != nil && (err == gorm.ErrRecordNotFound || strings.Contains(strings.ToLower(err.Error()), "record not found")) + return err != nil && (errors.Is(err, gorm.ErrRecordNotFound) || strings.Contains(strings.ToLower(err.Error()), "record not found")) } diff --git a/internal/apps/openflare/pages/source_orphan_cleanup.go b/internal/apps/openflare/pages/source_orphan_cleanup.go index 9de3bd9d..570eea86 100644 --- a/internal/apps/openflare/pages/source_orphan_cleanup.go +++ b/internal/apps/openflare/pages/source_orphan_cleanup.go @@ -111,6 +111,8 @@ func (summary *PagesOrphanCleanupSummary) add(outcome pagesOrphanCleanupOutcome) summary.LeaseBusy++ case pagesOrphanCleanupInvalidMarker: summary.InvalidMarker++ + case pagesOrphanCleanupSkipped: + summary.Skipped++ default: summary.Skipped++ } diff --git a/internal/apps/openflare/pages/source_scanner.go b/internal/apps/openflare/pages/source_scanner.go index 56722fef..06510e38 100644 --- a/internal/apps/openflare/pages/source_scanner.go +++ b/internal/apps/openflare/pages/source_scanner.go @@ -266,6 +266,8 @@ func scanOneDueGitHubSource( case sourceLeaseStale: summary.StaleSources++ return + case sourceLeaseAcquired: + // 获取执行权成功,继续执行扫描。 } if snapshot == nil || snapshot.SourceType != PagesSourceTypeGitHubRelease || snapshot.ReleaseSelector != githubReleaseSelectorLatest { diff --git a/internal/apps/openflare/pages/source_sync.go b/internal/apps/openflare/pages/source_sync.go index 68293777..117ebaf2 100644 --- a/internal/apps/openflare/pages/source_sync.go +++ b/internal/apps/openflare/pages/source_sync.go @@ -9,7 +9,7 @@ import ( "errors" "fmt" "path" - "sort" + "slices" "strings" "sync" "time" @@ -611,7 +611,7 @@ func lockSourceDeploymentUploadsTx( if hasIngest && ingestResult.Upload.ID != 0 && ingestResult.Upload.ID != target.UploadID { uploadIDs = append(uploadIDs, ingestResult.Upload.ID) } - sort.Slice(uploadIDs, func(i, j int) bool { return uploadIDs[i] < uploadIDs[j] }) + slices.Sort(uploadIDs) var records []model.Upload if err := tx.Clauses(clause.Locking{Strength: pagesRowLockStrength}). Where("id IN ?", uploadIDs). diff --git a/internal/apps/openflare/pages/source_tasks.go b/internal/apps/openflare/pages/source_tasks.go index a8248328..9d3b314a 100644 --- a/internal/apps/openflare/pages/source_tasks.go +++ b/internal/apps/openflare/pages/source_tasks.go @@ -154,10 +154,8 @@ func (h *SourceActionHandler) Execute(ctx context.Context, payload []byte) (*tas if source.SourceType != PagesSourceTypeRemoteURL && source.SourceType != PagesSourceTypeGitHubRelease { return nil, task.PermanentError(errPagesSourceTypeUnsupported) } - if source.SourceType == PagesSourceTypeRemoteURL && (input.TargetRevision != "" || input.ConfirmedRevision != "") { - return nil, task.PermanentError(errPagesSourceActionInvalid) - } - if input.Action == sourceActionCheck && (input.TargetRevision != "" || input.ConfirmedRevision != "") { + if (source.SourceType == PagesSourceTypeRemoteURL || input.Action == sourceActionCheck) && + (input.TargetRevision != "" || input.ConfirmedRevision != "") { return nil, task.PermanentError(errPagesSourceActionInvalid) } @@ -174,6 +172,8 @@ func (h *SourceActionHandler) Execute(ctx context.Context, payload []byte) (*tas case sourceLeaseStale: task.AppendLog(ctx, "[resolve] 来源配置或执行权已变化,本次任务跳过") return &task.TaskResult{Message: errPagesSourceActionStale}, nil + case sourceLeaseAcquired: + // 已获取执行权,继续执行。 } if input.Action == sourceActionCheck { diff --git a/internal/apps/openflare/proxy_route/helpers.go b/internal/apps/openflare/proxy_route/helpers.go index e5f07a4a..98418483 100644 --- a/internal/apps/openflare/proxy_route/helpers.go +++ b/internal/apps/openflare/proxy_route/helpers.go @@ -86,8 +86,8 @@ func validateOriginAddress(address string) error { if len(address) > maxOriginHostnameLength { return errors.New(errProxyRouteOriginInvalid) } - labels := strings.Split(address, ".") - for _, label := range labels { + labels := strings.SplitSeq(address, ".") + for label := range labels { if len(label) == 0 || len(label) > 63 { return errors.New(errProxyRouteOriginInvalid) } @@ -167,8 +167,8 @@ func buildOriginURLFromParts(scheme, address, port, uri string) (string, error) Host: formatOriginHost(normalizedAddress, normalizedPort), } if normalizedURI != "" { - if strings.HasPrefix(normalizedURI, "?") { - parsed.RawQuery = strings.TrimPrefix(normalizedURI, "?") + if after, ok := strings.CutPrefix(normalizedURI, "?"); ok { + parsed.RawQuery = after } else { pathQuery := strings.SplitN(normalizedURI, "?", originURIPathQueryParts) parsed.Path = pathQuery[0] diff --git a/internal/apps/openflare/proxy_route/logics.go b/internal/apps/openflare/proxy_route/logics.go index f4890e3b..da28846b 100644 --- a/internal/apps/openflare/proxy_route/logics.go +++ b/internal/apps/openflare/proxy_route/logics.go @@ -6,7 +6,7 @@ package proxy_route import ( "context" "errors" - "sort" + "slices" "strings" "time" @@ -120,7 +120,7 @@ func GetProxyRoute(ctx context.Context, id uint) (*View, error) { // CreateProxyRoute 创建代理规则。 func CreateProxyRoute(ctx context.Context, input Input) (*View, error) { - route, _, err := buildProxyRoute(ctx, nil, input) + route, err := buildProxyRoute(ctx, nil, input) if err != nil { return nil, err } @@ -148,7 +148,7 @@ func UpdateProxyRoute(ctx context.Context, id uint, input Input) (*View, error) return nil, err } previousPagesProjectID := pagesProjectIDForRoute(route) - route, _, err = buildProxyRoute(ctx, route, input) + route, err = buildProxyRoute(ctx, route, input) if err != nil { return nil, err } @@ -201,7 +201,7 @@ func lockPagesProjectsForRouteMutation(tx *gorm.DB, previousProjectID uint, rout if nextProjectID != 0 && nextProjectID != previousProjectID { projectIDs = append(projectIDs, nextProjectID) } - sort.Slice(projectIDs, func(i int, j int) bool { return projectIDs[i] < projectIDs[j] }) + slices.Sort(projectIDs) for _, projectID := range projectIDs { project, err := repository.LockPagesProjectByIDTx(tx, projectID) @@ -244,67 +244,67 @@ func DeleteProxyRoute(ctx context.Context, id uint) error { return repository.DeleteProxyRouteAndUnbind(ctx, id) } -func buildProxyRoute(ctx context.Context, route *model.ProxyRoute, input Input) (*model.ProxyRoute, []model.ZoneDomain, error) { +func buildProxyRoute(ctx context.Context, route *model.ProxyRoute, input Input) (*model.ProxyRoute, error) { domains, err := loadProxyRouteZoneDomains(ctx, input.ZoneDomainIDs) if err != nil { - return nil, nil, err + return nil, err } siteName := strings.TrimSpace(input.SiteName) upstreamType := normalizeUpstreamType(input.UpstreamType) _, originID, upstreams, err := resolveProxyRouteUpstreams(ctx, upstreamType, input) if err != nil { - return nil, nil, err + return nil, err } originHost := strings.TrimSpace(input.OriginHost) cachePolicy := strings.TrimSpace(input.CachePolicy) cacheRules, err := normalizeCacheRules(input.CacheEnabled, cachePolicy, input.CacheRules) if err != nil { - return nil, nil, err + return nil, err } customHeaders, err := normalizeCustomHeaders(input.CustomHeaders) if err != nil { - return nil, nil, err + return nil, err } limitConnPerServer, err := normalizeProxyRouteLimitConnValue(input.LimitConnPerServer, "limit_conn_per_server") if err != nil { - return nil, nil, err + return nil, err } limitConnPerIP, err := normalizeProxyRouteLimitConnValue(input.LimitConnPerIP, "limit_conn_per_ip") if err != nil { - return nil, nil, err + return nil, err } limitRate, err := normalizeProxyRouteLimitRate(input.LimitRate) if err != nil { - return nil, nil, err + return nil, err } limitReqPerIP, err := normalizeProxyRouteLimitReqPerIP(input.LimitReqPerIP) if err != nil { - return nil, nil, err + return nil, err } if err := validateProxyRouteZoneDomainCertificates(ctx, domains, input.EnableHTTPS); err != nil { - return nil, nil, err + return nil, err } jsonFields, err := marshalProxyRouteJSONFields(upstreams, cacheRules, customHeaders) if err != nil { - return nil, nil, err + return nil, err } if err := validateProxyRouteSiteName(siteName); err != nil { - return nil, nil, err + return nil, err } if err := validateProxyRouteSiteNameUniqueness(ctx, route, siteName); err != nil { - return nil, nil, err + return nil, err } if err := validateOriginHost(originHost); err != nil { - return nil, nil, err + return nil, err } if input.RedirectHTTP && !input.EnableHTTPS { - return nil, nil, errors.New(errProxyRouteRedirectHTTP) + return nil, errors.New(errProxyRouteRedirectHTTP) } if err := normalizeProxyRouteBasicAuth(&input); err != nil { - return nil, nil, err + return nil, err } if route == nil { @@ -326,9 +326,9 @@ func buildProxyRoute(ctx context.Context, route *model.ProxyRoute, input Input) upstreamType, ) if err := applyProxyRouteUpstreamType(ctx, route, upstreamType, input); err != nil { - return nil, nil, err + return nil, err } - return route, domains, nil + return route, nil } func buildProxyRouteViews(ctx context.Context, routes []*model.ProxyRoute) ([]*View, error) { diff --git a/internal/apps/openflare/proxy_route/logics_test.go b/internal/apps/openflare/proxy_route/logics_test.go index 2b3bb203..c2011d46 100644 --- a/internal/apps/openflare/proxy_route/logics_test.go +++ b/internal/apps/openflare/proxy_route/logics_test.go @@ -136,7 +136,7 @@ func TestRouteCanMoveAwayFromAlreadyMissingPagesProject(t *testing.T) { } func TestNormalizeCachePolicyDefaultsAndLegacy(t *testing.T) { - assert.Equal(t, "", normalizeCachePolicy(false, "static")) + assert.Empty(t, normalizeCachePolicy(false, "static")) // Empty/url on write = legacy all (compat); UI sends static explicitly for new default. assert.Equal(t, proxyRouteCachePolicyAll, normalizeCachePolicy(true, "")) assert.Equal(t, proxyRouteCachePolicyStatic, normalizeCachePolicy(true, "static")) @@ -144,7 +144,7 @@ func TestNormalizeCachePolicyDefaultsAndLegacy(t *testing.T) { assert.Equal(t, proxyRouteCachePolicyAll, normalizeCachePolicy(true, "all")) assert.Equal(t, proxyRouteCachePolicySuffix, normalizeCachePolicy(true, "suffix")) - assert.Equal(t, "", displayCachePolicy(false, "all")) + assert.Empty(t, displayCachePolicy(false, "all")) assert.Equal(t, proxyRouteCachePolicyAll, displayCachePolicy(true, "")) assert.Equal(t, proxyRouteCachePolicyAll, displayCachePolicy(true, "url")) assert.Equal(t, proxyRouteCachePolicyStatic, displayCachePolicy(true, "static")) diff --git a/internal/apps/openflare/relay/logics.go b/internal/apps/openflare/relay/logics.go index 5339c26f..12f1c462 100644 --- a/internal/apps/openflare/relay/logics.go +++ b/internal/apps/openflare/relay/logics.go @@ -5,6 +5,7 @@ package relay import ( "context" + "errors" "fmt" "strings" "time" @@ -20,7 +21,7 @@ const nodeStatusOnline = "online" // Heartbeat processes a relay heartbeat, updates node status, and returns config. func Heartbeat(ctx context.Context, node *model.OpenFlareNode, payload HeartbeatPayload) (*HeartbeatResponse, error) { if node == nil { - return nil, fmt.Errorf("relay node is nil") + return nil, errors.New("relay node is nil") } payload.Version = strings.TrimSpace(payload.Version) diff --git a/internal/apps/openflare/relay/logics_test.go b/internal/apps/openflare/relay/logics_test.go index f40481a1..729226d7 100644 --- a/internal/apps/openflare/relay/logics_test.go +++ b/internal/apps/openflare/relay/logics_test.go @@ -117,7 +117,7 @@ func TestHeartbeatPayloadBindingAndFrpsObservationInsert(t *testing.T) { snapshots, err := repository.ListOpenFlareMetricSnapshotsSince(ctx, node.NodeID, now.Add(-time.Minute), 10) require.NoError(t, err) require.Len(t, snapshots, 1) - assert.Equal(t, 12.5, snapshots[0].CPUUsagePercent) + assert.InDelta(t, 12.5, snapshots[0].CPUUsagePercent, 1e-9) frpsObs, err := repository.ListOpenFlareNodeObservationFrps(ctx, node.NodeID, time.Time{}, 1) require.NoError(t, err) diff --git a/internal/apps/openflare/relay/routers.go b/internal/apps/openflare/relay/routers.go index 99edc5ac..67dfcf9f 100644 --- a/internal/apps/openflare/relay/routers.go +++ b/internal/apps/openflare/relay/routers.go @@ -38,7 +38,11 @@ func PostHeartbeat(c *gin.Context) { response.AbortUnauthorized(c, errAgentTokenInvalid) return } - node := authNode.(*model.OpenFlareNode) + node, ok := authNode.(*model.OpenFlareNode) + if !ok { + response.AbortUnauthorized(c, errAgentTokenInvalid) + return + } result, err := Heartbeat(c.Request.Context(), node, payload) if apiutil.AbortBadRequestOnError(c, err) { @@ -61,6 +65,10 @@ func GetWebSocket(c *gin.Context) { response.AbortUnauthorized(c, errAgentTokenInvalid) return } - node := authNode.(*model.OpenFlareNode) + node, ok := authNode.(*model.OpenFlareNode) + if !ok { + response.AbortUnauthorized(c, errAgentTokenInvalid) + return + } ofws.ServeRelay(c, node.NodeID) } diff --git a/internal/apps/openflare/tasks/ssl_renew_test.go b/internal/apps/openflare/tasks/ssl_renew_test.go index f79b8d7f..f298a269 100644 --- a/internal/apps/openflare/tasks/ssl_renew_test.go +++ b/internal/apps/openflare/tasks/ssl_renew_test.go @@ -8,17 +8,16 @@ import ( "testing" "time" - "github.com/Rain-kl/Wavelet/internal/repository" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/tls" "github.com/Rain-kl/Wavelet/internal/infra/config" db "github.com/Rain-kl/Wavelet/internal/infra/persistence" "github.com/Rain-kl/Wavelet/internal/infra/task" "github.com/Rain-kl/Wavelet/internal/model" - "github.com/glebarez/sqlite" + "github.com/Rain-kl/Wavelet/internal/repository" + "github.com/Rain-kl/Wavelet/internal/testhelper" + "github.com/hibiken/asynq" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gorm.io/gorm" ) func setupSSLRenewTestDB(t *testing.T) func() { @@ -26,18 +25,23 @@ func setupSSLRenewTestDB(t *testing.T) func() { task.RegisterTaskMeta(tls.SSLSingleRenewMeta) - sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ - DisableForeignKeyConstraintWhenMigrating: true, - }) - require.NoError(t, err) - require.NoError(t, sqliteDB.AutoMigrate(&model.TLSCertificate{}, &model.TaskExecution{})) + _, mr, cleanup := testhelper.SetupTestEnvironment(t) + require.NoError(t, db.DB(nil).AutoMigrate(&model.TLSCertificate{}, &model.TaskExecution{})) + + // task 包 init() 会按配置创建指向真实 Redis 的客户端;测试显式改用 + // miniredis(与 executor_test 一致),避免依赖本地 redis 实例。 + oldClient := task.AsynqClient + task.AsynqClient = asynq.NewClient(asynq.RedisClientOpt{Addr: mr.Addr()}) + t.Cleanup(func() { + _ = task.AsynqClient.Close() + task.AsynqClient = oldClient + }) - db.SetDB(sqliteDB) oldSecret := config.Config.App.SessionSecret config.Config.App.SessionSecret = "test_session_secret_for_ssl_renew" return func() { - db.SetDB(nil) config.Config.App.SessionSecret = oldSecret + cleanup() } } diff --git a/internal/apps/openflare/tls/acme/client.go b/internal/apps/openflare/tls/acme/client.go index 7b1487e4..98e62e9f 100644 --- a/internal/apps/openflare/tls/acme/client.go +++ b/internal/apps/openflare/tls/acme/client.go @@ -173,7 +173,7 @@ func SetupDNSProvider(client *lego.Client, dnsType, dnsAuth string, dns1, dns2 s case "cloudflare": var creds map[string]string if err := json.Unmarshal([]byte(dnsAuth), &creds); err != nil { - return fmt.Errorf("failed to parse cloudflare credentials: %v", err) + return fmt.Errorf("failed to parse cloudflare credentials: %w", err) } config := cloudflare.NewDefaultConfig() diff --git a/internal/apps/openflare/tls/acme_obtain_test.go b/internal/apps/openflare/tls/acme_obtain_test.go index d291ecce..5bee84a0 100644 --- a/internal/apps/openflare/tls/acme_obtain_test.go +++ b/internal/apps/openflare/tls/acme_obtain_test.go @@ -5,7 +5,6 @@ package tls import ( "context" - "strings" "testing" "time" @@ -51,10 +50,19 @@ func TestApplyCertificateReturnsApplying(t *testing.T) { }) require.NoError(t, err) + obtainDone := make(chan struct{}) restore := SetObtainCertificateFuncForTest(func(ctx context.Context, cert *model.TLSCertificate) error { + defer close(obtainDone) return updateCertError(ctx, cert, "dns challenge failed") }) - defer restore() + defer func() { + select { + case <-obtainDone: + case <-time.After(2 * time.Second): + t.Fatal("async certificate obtain did not finish") + } + restore() + }() cert, err := ApplyCertificate(ctx, ApplyInput{ Name: "Test ACME Cert", @@ -126,7 +134,7 @@ func TestConvertCertificateToACMEPreservesUploadOnFailure(t *testing.T) { assert.Equal(t, "error", finalCert.ApplyStatus) assert.Equal(t, originalStoredCertPEM, finalCert.CertPEM) assert.Equal(t, originalStoredKeyPEM, finalCert.KeyPEM) - assert.True(t, strings.Contains(finalCert.ApplyMessage, "dns challenge failed")) + assert.Contains(t, finalCert.ApplyMessage, "dns challenge failed") } func TestConvertCertificateToACMERejectsInvalidStates(t *testing.T) { diff --git a/internal/apps/openflare/tls/logics.go b/internal/apps/openflare/tls/logics.go index 9fc5c7c2..60b427c0 100644 --- a/internal/apps/openflare/tls/logics.go +++ b/internal/apps/openflare/tls/logics.go @@ -197,12 +197,17 @@ func ApplyCertificate(ctx context.Context, input ApplyInput) (*model.TLSCertific return nil, err } + // 先取响应快照再启动异步续签:sanitize 会整体拷贝 cert,若与异步 goroutine + // 的字段写入并发会构成数据竞争(生产真实问题)。 + returned := sanitizeCertificateForResponse(cert) + + obtainFn := obtainTLSCertificate // 捕获当前实现,避免 goroutine 内读可变包变量(测试热替换) go func(c *model.TLSCertificate) { asyncCtx := context.WithoutCancel(ctx) - _ = obtainTLSCertificate(asyncCtx, c) + _ = obtainFn(asyncCtx, c) }(cert) - return sanitizeCertificateForResponse(cert), nil + return returned, nil } // UpdateACMECertificate 更新 ACME 证书配置。 @@ -225,12 +230,15 @@ func UpdateACMECertificate(ctx context.Context, id uint, input ApplyInput) (*mod return nil, err } + returned := sanitizeCertificateForResponse(cert) + + obtainFn := obtainTLSCertificate // 捕获当前实现,避免 goroutine 内读可变包变量(测试热替换) go func(c *model.TLSCertificate) { asyncCtx := context.WithoutCancel(ctx) - _ = obtainTLSCertificate(asyncCtx, c) + _ = obtainFn(asyncCtx, c) }(cert) - return sanitizeCertificateForResponse(cert), nil + return returned, nil } // ConvertCertificateToACME 将上传证书转为 ACME 管理。 @@ -257,9 +265,10 @@ func ConvertCertificateToACME(ctx context.Context, id uint, input ApplyInput) (* return nil, err } + obtainFn := obtainTLSCertificate // 捕获当前实现,避免 goroutine 内读可变包变量(测试热替换) go func(c *model.TLSCertificate) { asyncCtx := context.WithoutCancel(ctx) - if err := obtainTLSCertificate(asyncCtx, c); err != nil { + if err := obtainFn(asyncCtx, c); err != nil { return } latest, err := repository.GetTLSCertificateByID(asyncCtx, c.ID) diff --git a/internal/apps/openflare/tls/obtain.go b/internal/apps/openflare/tls/obtain.go index 266c72dc..89c0a0f7 100644 --- a/internal/apps/openflare/tls/obtain.go +++ b/internal/apps/openflare/tls/obtain.go @@ -123,7 +123,7 @@ func splitAcmeDomains(primaryDomain, otherDomains string) []string { if !strings.Contains(otherDomains, "\n") && strings.Contains(otherDomains, ",") { separator = "," } - for _, domain := range strings.Split(otherDomains, separator) { + for domain := range strings.SplitSeq(otherDomains, separator) { domain = strings.TrimSpace(domain) if domain != "" { domains = append(domains, domain) diff --git a/internal/apps/openflare/uptimekuma/client.go b/internal/apps/openflare/uptimekuma/client.go index 14f182c5..48d6bcfc 100644 --- a/internal/apps/openflare/uptimekuma/client.go +++ b/internal/apps/openflare/uptimekuma/client.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "log/slog" + "maps" "net/http" "strconv" "strings" @@ -88,7 +89,7 @@ func NewSocketIOClient(baseURL string) *SocketIOClient { // Connect performs the Engine.IO handshake and starts the polling loop. func (c *SocketIOClient) Connect() error { slog.Debug("Uptime Kuma client starting handshake", "baseURL", c.baseURL) - u := fmt.Sprintf("%s/socket.io/?EIO=4&transport=polling", c.baseURL) + u := c.baseURL + "/socket.io/?EIO=4&transport=polling" reqHandshake, err := http.NewRequestWithContext(c.ctx, http.MethodGet, u, nil) if err != nil { return fmt.Errorf("create handshake request failed: %w", err) @@ -180,8 +181,8 @@ func (c *SocketIOClient) pollLoop() { } slog.Debug("Received polling payload from Uptime Kuma", "length", len(bodyStr)) - packets := strings.Split(bodyStr, "\x1e") - for _, pkt := range packets { + packets := strings.SplitSeq(bodyStr, "\x1e") + for pkt := range packets { if len(pkt) == 0 { continue } @@ -280,7 +281,8 @@ func (c *SocketIOClient) Emit(event string, args ...any) (string, error) { c.ackChanMap[id] = ch c.ackMutex.Unlock() - payloadArr := []any{event} + payloadArr := make([]any, 1, 1+len(args)) + payloadArr[0] = event payloadArr = append(payloadArr, args...) bs, err := json.Marshal(payloadArr) if err != nil { @@ -352,9 +354,7 @@ func (c *SocketIOClient) GetMonitorList() map[string]Monitor { defer c.monitorListMutex.RUnlock() m := make(map[string]Monitor, len(c.monitorList)) - for k, v := range c.monitorList { - m[k] = v - } + maps.Copy(m, c.monitorList) return m } diff --git a/internal/apps/openflare/uptimekuma/sync.go b/internal/apps/openflare/uptimekuma/sync.go index 062b1fc2..e33e00d5 100644 --- a/internal/apps/openflare/uptimekuma/sync.go +++ b/internal/apps/openflare/uptimekuma/sync.go @@ -5,6 +5,7 @@ package uptimekuma import ( "context" + "errors" "fmt" "log/slog" "strings" @@ -32,7 +33,7 @@ type kumaConfig struct { } // loadKumaConfig 从 SystemConfig 加载 UptimeKuma 配置 -func loadKumaConfig(ctx context.Context) (*kumaConfig, error) { +func loadKumaConfig(ctx context.Context) *kumaConfig { url, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyUptimeKumaURL) username, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyUptimeKumaUsername) password, _ := repository.GetSystemConfigByKey(ctx, model.ConfigKeyUptimeKumaPassword) @@ -67,7 +68,7 @@ func loadKumaConfig(ctx context.Context) (*kumaConfig, error) { Retry: retry, RetryInterval: retryInterval, Timeout: timeout, - }, nil + } } // SyncToUptimeKuma synchronizes enabled proxy routes to Uptime Kuma monitors. @@ -75,19 +76,16 @@ func SyncToUptimeKuma(ctx context.Context) error { // 检查是否启用 enabled, _ := repository.GetBoolByKey(ctx, model.ConfigKeyUptimeKumaEnabled) if !enabled { - return fmt.Errorf("uptime Kuma integration is disabled") + return errors.New("uptime Kuma integration is disabled") } if !isSyncing.CompareAndSwap(false, true) { - return fmt.Errorf("sync task is already in progress, please try again later") + return errors.New("sync task is already in progress, please try again later") } defer isSyncing.Store(false) // 加载配置 - config, err := loadKumaConfig(ctx) - if err != nil { - return err - } + config := loadKumaConfig(ctx) // 验证配置 if err := validateKumaConfig(config); err != nil { @@ -105,10 +103,7 @@ func SyncToUptimeKuma(ctx context.Context) error { return fmt.Errorf("failed to list local proxy routes: %w", err) } - expectedRoutes, err := filterExpectedRoutes(allRoutes, config) - if err != nil { - return err - } + expectedRoutes := filterExpectedRoutes(allRoutes, config) client, err := connectAndLoginUptimeKuma(config.URL, config.Username, config.Password) if err != nil { @@ -128,7 +123,7 @@ func SyncToUptimeKuma(ctx context.Context) error { return nil } -func filterExpectedRoutes(allRoutes []*model.ProxyRoute, config *kumaConfig) ([]*model.ProxyRoute, error) { +func filterExpectedRoutes(allRoutes []*model.ProxyRoute, config *kumaConfig) []*model.ProxyRoute { scope := config.MonitorScope if scope == "selected" { selectedList := strings.Split(config.SelectedSites, ",") @@ -145,7 +140,7 @@ func filterExpectedRoutes(allRoutes []*model.ProxyRoute, config *kumaConfig) ([] expectedRoutes = append(expectedRoutes, route) } } - return expectedRoutes, nil + return expectedRoutes } var expectedRoutes []*model.ProxyRoute @@ -154,7 +149,7 @@ func filterExpectedRoutes(allRoutes []*model.ProxyRoute, config *kumaConfig) ([] expectedRoutes = append(expectedRoutes, route) } } - return expectedRoutes, nil + return expectedRoutes } func ensureOpenFlareTag(client *SocketIOClient) (int, error) { @@ -225,7 +220,7 @@ func filterOpenFlareMonitors(monitors map[string]Monitor, openFlareTagID int) ma func routeMonitorURL(ctx context.Context, route *model.ProxyRoute) (string, error) { if route == nil { - return "", fmt.Errorf("proxy route is nil") + return "", errors.New("proxy route is nil") } domains, err := repository.ListZoneDomainsByRouteID(ctx, route.ID) if err != nil { diff --git a/internal/apps/openflare/uptimekuma/sync_helpers.go b/internal/apps/openflare/uptimekuma/sync_helpers.go index 14f7d63f..2a184805 100644 --- a/internal/apps/openflare/uptimekuma/sync_helpers.go +++ b/internal/apps/openflare/uptimekuma/sync_helpers.go @@ -5,6 +5,7 @@ package uptimekuma import ( "context" + "errors" "fmt" "log/slog" "strings" @@ -18,13 +19,13 @@ const monitorListWaitTimeout = 5 * time.Second // validateKumaConfig 验证 kumaConfig 配置完整性 func validateKumaConfig(config *kumaConfig) error { if strings.TrimSpace(config.URL) == "" { - return fmt.Errorf("uptime Kuma URL is not configured") + return errors.New("uptime Kuma URL is not configured") } if strings.TrimSpace(config.Username) == "" { - return fmt.Errorf("uptime Kuma username is not configured") + return errors.New("uptime Kuma username is not configured") } if strings.TrimSpace(config.Password) == "" { - return fmt.Errorf("uptime Kuma password is not configured") + return errors.New("uptime Kuma password is not configured") } return nil } @@ -65,7 +66,7 @@ func connectAndLoginUptimeKuma(kumaURL, kumaUsername, kumaPassword string) (*Soc case <-time.After(monitorListWaitTimeout): client.Close() slog.Error("Timeout waiting for Uptime Kuma monitorList push event") - return nil, fmt.Errorf("timeout waiting for monitorList event from Uptime Kuma") + return nil, errors.New("timeout waiting for monitorList event from Uptime Kuma") } return client, nil } diff --git a/internal/apps/openflare/waf/graph_compile.go b/internal/apps/openflare/waf/graph_compile.go index 42223630..7a36b761 100644 --- a/internal/apps/openflare/waf/graph_compile.go +++ b/internal/apps/openflare/waf/graph_compile.go @@ -4,7 +4,9 @@ package waf import ( + "errors" "fmt" + "slices" "sort" ) @@ -34,14 +36,14 @@ func CompileRuleGraph(graph RuleGraph) (RuntimeRuleGraph, error) { } if node.Type == RuleNodeStart { if runtime.Entry != "" { - return RuntimeRuleGraph{}, fmt.Errorf("规则图包含多个开始节点") + return RuntimeRuleGraph{}, errors.New("规则图包含多个开始节点") } runtime.Entry = node.ID } runtime.Nodes[node.ID] = RuntimeRuleNode{Type: node.Type, Config: config} } if runtime.Entry == "" { - return RuntimeRuleGraph{}, fmt.Errorf("规则图缺少开始节点") + return RuntimeRuleGraph{}, errors.New("规则图缺少开始节点") } for _, edge := range graph.Edges { node, ok := runtime.Nodes[edge.Source] @@ -142,7 +144,7 @@ func sortedUniqueStrings(values []string) []string { func sortedUniqueUints(values []uint) []uint { result := append([]uint(nil), values...) - sort.Slice(result, func(i, j int) bool { return result[i] < result[j] }) + slices.Sort(result) write := 0 for _, value := range result { if write == 0 || result[write-1] != value { diff --git a/internal/apps/openflare/waf/graph_validate.go b/internal/apps/openflare/waf/graph_validate.go index ddab7fef..2c9dc310 100644 --- a/internal/apps/openflare/waf/graph_validate.go +++ b/internal/apps/openflare/waf/graph_validate.go @@ -12,6 +12,7 @@ import ( "io" "net/netip" "regexp" + "slices" "strings" ) @@ -67,7 +68,7 @@ func validateRuleGraphLimits(graph RuleGraph) error { if raw, err := json.Marshal(graph); err != nil { return fmt.Errorf("规则图无法序列化: %w", err) } else if len(raw) > maxRuleGraphBytes { - return fmt.Errorf("规则图大小不能超过 256 KiB") + return errors.New("规则图大小不能超过 256 KiB") } return nil } @@ -374,12 +375,7 @@ func decodeStrictConfig(raw json.RawMessage, dst any) error { } func validSourceHandle(t RuleNodeType, handle string) bool { - for _, expected := range requiredHandles(t) { - if handle == expected { - return true - } - } - return false + return slices.Contains(requiredHandles(t), handle) } func requiredHandles(t RuleNodeType) []string { switch t { @@ -387,6 +383,8 @@ func requiredHandles(t RuleNodeType) []string { return []string{"next"} case RuleNodeIPMatch, RuleNodeGeoMatch, RuleNodeUACheck, RuleNodeSecurityCheck: return []string{"true", "false"} + case RuleNodeAllow, RuleNodeBlock: + return nil default: return nil } diff --git a/internal/apps/openflare/waf/ip_group_sync.go b/internal/apps/openflare/waf/ip_group_sync.go index 1016b556..5738b6b4 100644 --- a/internal/apps/openflare/waf/ip_group_sync.go +++ b/internal/apps/openflare/waf/ip_group_sync.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "log/slog" + "maps" "net" "net/http" "net/netip" @@ -325,9 +326,7 @@ func evaluateParsedIPGroupAutoConfig(ctx context.Context, config ipGroupAutoConf lastSeen = time.Unix(item.LastSeenEpoch, 0).UTC() } statusCounts := make(map[int]int, len(item.StatusCounts)) - for code, count := range item.StatusCounts { - statusCounts[code] = count - } + maps.Copy(statusCounts, item.StatusCounts) accumulators[ip] = &ipGroupAutoAccumulator{ ip: ip, requestCount: item.RequestCount, @@ -404,7 +403,7 @@ func downloadIPGroupSubscription(ctx context.Context, rawURL string) ([]byte, er return nil, err } client := http.Client{Timeout: 15 * time.Second} - req, err := http.NewRequestWithContext(ctx, "GET", rawURL, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil) if err != nil { return nil, fmt.Errorf("下载订阅失败: %w", err) } @@ -479,7 +478,7 @@ func selectJSONMappingNodes(payload any, mappingRule string) ([]any, error) { } rule = strings.TrimPrefix(rule, "$.") nodes := []any{payload} - for _, rawSegment := range strings.Split(rule, ".") { + for rawSegment := range strings.SplitSeq(rule, ".") { segment := strings.TrimSpace(rawSegment) if segment == "" { continue diff --git a/internal/apps/openflare/waf/rule_logics_test.go b/internal/apps/openflare/waf/rule_logics_test.go index 71546a2d..b4376b83 100644 --- a/internal/apps/openflare/waf/rule_logics_test.go +++ b/internal/apps/openflare/waf/rule_logics_test.go @@ -7,7 +7,6 @@ import ( "bytes" "context" "encoding/json" - "errors" "net/http" "net/http/httptest" "strconv" @@ -63,12 +62,14 @@ func TestRuleHandlersMapFailures(t *testing.T) { {name: "invalid id", method: http.MethodGet, path: "/rules/nope", setup: setupWAFTestDB, want: http.StatusBadRequest}, {name: "malformed json", method: http.MethodPost, path: "/rules", body: `{`, setup: setupWAFTestDB, want: http.StatusBadRequest}, {name: "invalid graph", method: http.MethodPost, path: "/rules/1/graph", body: `{"revision":1,"graph":{"schema_version":1,"nodes":[],"edges":[]}}`, setup: func(t *testing.T) func() { + t.Helper() cleanup := setupWAFTestDB(t) _, err := CreateRule(context.Background(), CreateRuleInput{Name: "one"}) require.NoError(t, err) return cleanup }, want: http.StatusBadRequest}, {name: "manual IP group sync", method: http.MethodPost, path: "/ip-groups/1/sync", setup: func(t *testing.T) func() { + t.Helper() cleanup := setupWAFTestDB(t) _, err := CreateIPGroup(context.Background(), IPGroupInput{Name: "manual", Type: wafIPGroupTypeManual, Enabled: true}) require.NoError(t, err) @@ -76,12 +77,13 @@ func TestRuleHandlersMapFailures(t *testing.T) { }, want: http.StatusBadRequest}, {name: "missing", method: http.MethodGet, path: "/rules/999", setup: setupWAFTestDB, want: http.StatusNotFound}, {name: "conflict", method: http.MethodPost, path: "/rules/1/graph", body: mustGraphRequest(t, 0), setup: func(t *testing.T) func() { + t.Helper() cleanup := setupWAFTestDB(t) _, err := CreateRule(context.Background(), CreateRuleInput{Name: "one"}) require.NoError(t, err) return cleanup }, want: http.StatusConflict}, - {name: "database failure", method: http.MethodGet, path: "/rules", setup: func(t *testing.T) func() { db.SetDB(nil); return func() {} }, want: http.StatusInternalServerError}, + {name: "database failure", method: http.MethodGet, path: "/rules", setup: func(t *testing.T) func() { t.Helper(); db.SetDB(nil); return func() {} }, want: http.StatusInternalServerError}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -171,7 +173,7 @@ func TestReplaceSiteRuleGroupsPreservesOrderAndRejectsGlobal(t *testing.T) { require.NoError(t, err) _, err = ReplaceSiteRuleGroups(ctx, 7, []uint{global.ID, second.ID}) require.Error(t, err) - assert.False(t, errors.Is(err, model.ErrWAFRuleRevisionConflict)) + require.NotErrorIs(t, err, model.ErrWAFRuleRevisionConflict) assert.Equal(t, []uint{third.ID, first.ID, second.ID}, mustListSiteRuleGroupIDs(t, ctx, 7)) } diff --git a/internal/apps/openflare/zone/legacy_import.go b/internal/apps/openflare/zone/legacy_import.go index 938d52a7..ed7c09f3 100644 --- a/internal/apps/openflare/zone/legacy_import.go +++ b/internal/apps/openflare/zone/legacy_import.go @@ -103,7 +103,7 @@ func ImportLegacyTx(ctx context.Context, tx *sql.Tx, postgres bool) (report Impo `), domain).Scan(&existingID, &existingZoneDomain) if scanErr == nil { if existingZoneDomain != root { - report.Conflicts = append(report.Conflicts, fmt.Sprintf("%s: global domain conflict", domain)) + report.Conflicts = append(report.Conflicts, domain+": global domain conflict") } else if item.ProxyRouteID != nil { if _, bindErr := tx.ExecContext(ctx, q(` UPDATE of_zone_domains @@ -147,7 +147,7 @@ func ImportLegacyTx(ctx context.Context, tx *sql.Tx, postgres bool) (report Impo } if len(report.Conflicts) > 0 { - return report, fmt.Errorf("legacy data has conflicts") + return report, errors.New("legacy data has conflicts") } return report, nil } @@ -292,7 +292,7 @@ func rebindSQL(query string, postgres bool) string { var b strings.Builder b.Grow(len(query) + len(query)/4) n := 0 - for i := 0; i < len(query); i++ { + for i := range len(query) { if query[i] == '?' { n++ b.WriteByte('$') diff --git a/internal/apps/openflare/zone/stats.go b/internal/apps/openflare/zone/stats.go index 6a5dcfc9..002b0abe 100644 --- a/internal/apps/openflare/zone/stats.go +++ b/internal/apps/openflare/zone/stats.go @@ -183,10 +183,7 @@ func emptyStatsSeries(since, until time.Time, bucketMinutes int) []StatsPoint { } // Cap points to keep chart readable. maxPoints := 120 - capacity := int(end.Sub(start)/bucket) + 1 - if capacity > maxPoints { - capacity = maxPoints - } + capacity := min(int(end.Sub(start)/bucket)+1, maxPoints) points := make([]StatsPoint, 0, capacity) for cursor := start; !cursor.After(end) && len(points) < maxPoints; cursor = cursor.Add(bucket) { points = append(points, StatsPoint{BucketStartedAt: cursor}) diff --git a/internal/apps/relay/config/config.go b/internal/apps/relay/config/config.go index dd841c8e..1d9ce32e 100644 --- a/internal/apps/relay/config/config.go +++ b/internal/apps/relay/config/config.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package config loads and persists relay daemon configuration. package config diff --git a/internal/apps/relay/config/version.go b/internal/apps/relay/config/version.go index 2b9656fb..66711aff 100644 --- a/internal/apps/relay/config/version.go +++ b/internal/apps/relay/config/version.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package config // Version is the relay daemon build version string. diff --git a/internal/apps/relay/frps/manager.go b/internal/apps/relay/frps/manager.go index 0c9a5e92..ce244569 100644 --- a/internal/apps/relay/frps/manager.go +++ b/internal/apps/relay/frps/manager.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package frps manages the lifecycle of the frps reverse-proxy process: // rendering its TOML config, supervising the child process with exponential- // backoff restarts, and exposing runtime status to the heartbeat subsystem. @@ -240,7 +243,7 @@ func (m *Manager) supervise(ctx context.Context, generation uint64) { continue } - _ = os.WriteFile(m.pidPath, []byte(fmt.Sprintf("%d", cmd.Process.Pid)), frpsPidFilePerm) + _ = os.WriteFile(m.pidPath, fmt.Appendf(nil, "%d", cmd.Process.Pid), frpsPidFilePerm) m.cmd = cmd m.status = "healthy" diff --git a/internal/apps/relay/frps/manager_test.go b/internal/apps/relay/frps/manager_test.go index 251538c3..52b5a7c4 100644 --- a/internal/apps/relay/frps/manager_test.go +++ b/internal/apps/relay/frps/manager_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package frps import ( @@ -7,7 +10,7 @@ import ( "os/exec" "path/filepath" "strings" - "sync/atomic" + "syscall" "testing" "time" @@ -16,6 +19,7 @@ import ( // Helper to write control file for the dummy script func writeControl(t *testing.T, dir string, exitCode int, delaySeconds int) { + t.Helper() controlPath := filepath.Join(dir, "control.txt") content := fmt.Sprintf("%d %d\n", exitCode, delaySeconds) err := os.WriteFile(controlPath, []byte(content), 0644) @@ -26,6 +30,7 @@ func writeControl(t *testing.T, dir string, exitCode int, delaySeconds int) { // Setup a dummy executable script that reads control.txt to decide exit code and sleep duration func setupDummyScript(t *testing.T) (string, string) { + t.Helper() dir := t.TempDir() scriptPath := filepath.Join(dir, "dummy_frps") @@ -53,6 +58,7 @@ exit "${EXIT_CODE:-0}" // Helper to poll for status to eliminate timing flakiness in tests func assertStatusEventually(t *testing.T, m *Manager, expectedStatus string, timeout time.Duration) { + t.Helper() if timeout < 6*time.Second { timeout = 6 * time.Second } @@ -304,17 +310,17 @@ func TestSupervisorGenerationInterrupt(t *testing.T) { t.Error("expected old process killed and new command started") } - // Verify old process is actually killed - var cmd1Finished int32 - go func() { - _ = cmd1.Wait() - atomic.StoreInt32(&cmd1Finished, 1) - }() - - time.Sleep(200 * time.Millisecond) - if atomic.LoadInt32(&cmd1Finished) != 1 { - t.Error("expected first process to be killed") + // Verify old process is actually killed:不要对受管 Cmd 调用 Wait(旧 supervise + // goroutine 拥有 Wait 权,并发 Wait 会与 os/exec 内部状态竞争),改为探测 + // 进程是否已被收割(Signal(0) 在 Wait 后即报错)。 + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + if err := cmd1.Process.Signal(syscall.Signal(0)); err != nil { + return + } + time.Sleep(20 * time.Millisecond) } + t.Error("expected first process to be killed") } func TestUpdateConfigKillsOrphanProcessBeforeRestart(t *testing.T) { diff --git a/internal/apps/relay/heartbeat/service.go b/internal/apps/relay/heartbeat/service.go index 1d039f30..268f9569 100644 --- a/internal/apps/relay/heartbeat/service.go +++ b/internal/apps/relay/heartbeat/service.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package heartbeat sends periodic relay node status to the control plane. package heartbeat diff --git a/internal/apps/relay/httpclient/client.go b/internal/apps/relay/httpclient/client.go index 8176cc55..2d413ae3 100644 --- a/internal/apps/relay/httpclient/client.go +++ b/internal/apps/relay/httpclient/client.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package httpclient provides an HTTP client for relay control-plane communication. package httpclient diff --git a/internal/apps/relay/observability/collector.go b/internal/apps/relay/observability/collector.go index c314b401..76aeed0d 100644 --- a/internal/apps/relay/observability/collector.go +++ b/internal/apps/relay/observability/collector.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package observability collects relay node profile data for heartbeat reporting. package observability diff --git a/internal/apps/relay/relay/runner.go b/internal/apps/relay/relay/runner.go index 658e8dfe..8d484b21 100644 --- a/internal/apps/relay/relay/runner.go +++ b/internal/apps/relay/relay/runner.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package relay implements the relay node daemon runtime loop. package relay diff --git a/internal/apps/relay/state/store.go b/internal/apps/relay/state/store.go index dff9634b..b97c5011 100644 --- a/internal/apps/relay/state/store.go +++ b/internal/apps/relay/state/store.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package state persists relay runtime state to disk. package state diff --git a/internal/apps/relay/updater/updater.go b/internal/apps/relay/updater/updater.go index d413f2c0..8ff93ba2 100644 --- a/internal/apps/relay/updater/updater.go +++ b/internal/apps/relay/updater/updater.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package updater provides relay self-update integration with the edge updater. package updater diff --git a/internal/apps/relay/wsclient/client.go b/internal/apps/relay/wsclient/client.go index d71d83cc..94e7e6c4 100644 --- a/internal/apps/relay/wsclient/client.go +++ b/internal/apps/relay/wsclient/client.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package wsclient provides a WebSocket client for relay control-plane communication. package wsclient diff --git a/internal/apps/upload/cache/access_cache.go b/internal/apps/upload/cache/access_cache.go index a8526cb0..d513b1a5 100644 --- a/internal/apps/upload/cache/access_cache.go +++ b/internal/apps/upload/cache/access_cache.go @@ -52,12 +52,13 @@ func ensureAccessCacheListener() { } func startAccessCacheInvalidationListener() { - if db.Redis == nil { + redis := db.Redis // 调用方 goroutine 上捕获,避免 goroutine 内读可变全局(测试会替换 db.Redis) + if redis == nil { return } go func() { - pubsub := db.Redis.Subscribe( + pubsub := redis.Subscribe( context.Background(), objectstore.ConfigInvalidationChannel, fileAccessInvalidationChannel, diff --git a/internal/apps/upload/filesrv/file_server.go b/internal/apps/upload/filesrv/file_server.go index 04d0ac34..113d81c1 100644 --- a/internal/apps/upload/filesrv/file_server.go +++ b/internal/apps/upload/filesrv/file_server.go @@ -68,7 +68,8 @@ func ServeFileByID(c *gin.Context) { response.AbortNotFound(c, "文件记录未找到") return } - if _, ok := err.(*strconv.NumError); ok { + var numErr *strconv.NumError + if errors.As(err, &numErr) { response.AbortBadRequest(c, "无效的上传ID") return } @@ -133,6 +134,8 @@ func ServeUpload(c *gin.Context, upload *model.Upload) { return } fallthrough + case fileTypeVideo, fileTypeAudio, fileTypeOther: + serveOriginalWithConditionalCheck(c, upload) default: serveOriginalWithConditionalCheck(c, upload) } @@ -205,7 +208,10 @@ func EnsureCompressedImageCache( return nil, false, err } - res := result.(compressedImageCacheResult) + res, ok := result.(compressedImageCacheResult) + if !ok { + return nil, false, fmt.Errorf("image compression flight returned unexpected type %T", result) + } return res.bytes, res.cached, res.err } diff --git a/internal/apps/upload/filesrv/file_server_test.go b/internal/apps/upload/filesrv/file_server_test.go index 722d90a2..1ac072ae 100644 --- a/internal/apps/upload/filesrv/file_server_test.go +++ b/internal/apps/upload/filesrv/file_server_test.go @@ -357,6 +357,7 @@ func TestNormalizeImageQuality(t *testing.T) { } func configureLocalStorageRoot(t *testing.T, dbConn *gorm.DB, tempDir string) { + t.Helper() var sc model.SystemConfig if err := dbConn.Where("key = ?", model.ConfigKeyStorageConfig).First(&sc).Error; err != nil { t.Fatalf("failed to find storage config: %v", err) diff --git a/internal/apps/upload/handler/file_management.go b/internal/apps/upload/handler/file_management.go index 335f3202..5ca3b7dd 100644 --- a/internal/apps/upload/handler/file_management.go +++ b/internal/apps/upload/handler/file_management.go @@ -301,7 +301,7 @@ func UpdateMyFile(c *gin.Context) { response.AbortNotFound(c, "文件记录未找到") return } - if err == ingest.ErrForbidden { + if errors.Is(err, ingest.ErrForbidden) { response.AbortForbidden(c, "无权操作") return } diff --git a/internal/apps/upload/handler/routers.go b/internal/apps/upload/handler/routers.go index 2667611f..0fafb443 100644 --- a/internal/apps/upload/handler/routers.go +++ b/internal/apps/upload/handler/routers.go @@ -178,7 +178,8 @@ func DownloadFile(c *gin.Context) { response.AbortNotFound(c, "文件记录未找到") return } - if _, ok := err.(*strconv.NumError); ok { + var numErr *strconv.NumError + if errors.As(err, &numErr) { response.AbortBadRequest(c, shared.ErrInvalidFileID) return } @@ -204,7 +205,7 @@ func DownloadFile(c *gin.Context) { } } - c.Header("Content-Disposition", fmt.Sprintf("attachment; filename*=UTF-8''%s", url.PathEscape(fileName))) + c.Header("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape(fileName)) filesrv.ServeUpload(c, upload) } diff --git a/internal/apps/upload/handler/routers_test.go b/internal/apps/upload/handler/routers_test.go index 05f19501..3ce89d03 100644 --- a/internal/apps/upload/handler/routers_test.go +++ b/internal/apps/upload/handler/routers_test.go @@ -75,6 +75,7 @@ func setupTestRouter(authUser *model.User) *gin.Engine { } func createMultipartRequest(t *testing.T, fieldName, fileName string, fileContent []byte, extraFields map[string]string) (string, *bytes.Buffer) { + t.Helper() body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -1086,6 +1087,7 @@ func TestDeleteReservedUploadType(t *testing.T) { } func configureLocalStorageRoot(t *testing.T, dbConn *gorm.DB, tempDir string) { + t.Helper() var sc model.SystemConfig if err := dbConn.Where("key = ?", model.ConfigKeyStorageConfig).First(&sc).Error; err != nil { t.Fatalf("failed to find storage config: %v", err) diff --git a/internal/apps/upload/ingest/helpers.go b/internal/apps/upload/ingest/helpers.go index 5905bbf4..175e6abe 100644 --- a/internal/apps/upload/ingest/helpers.go +++ b/internal/apps/upload/ingest/helpers.go @@ -59,8 +59,8 @@ func validateAllowedExtension(ctx context.Context, ext string) error { return nil } - allowedExts := strings.Split(strings.ToLower(sc.Value), ",") - for _, allowedExt := range allowedExts { + allowedExts := strings.SplitSeq(strings.ToLower(sc.Value), ",") + for allowedExt := range allowedExts { if strings.TrimSpace(allowedExt) == ext { return nil } diff --git a/internal/apps/upload/ingest/ingest.go b/internal/apps/upload/ingest/ingest.go index 8aa8e1f2..cdda6ca1 100644 --- a/internal/apps/upload/ingest/ingest.go +++ b/internal/apps/upload/ingest/ingest.go @@ -54,6 +54,8 @@ func ingestWithHashPolicy(ctx context.Context, req Request) (Result, error) { return Result{}, ErrStorageReadOnly } return createDedupRecord(ctx, existing, req) + case PolicyCreate: + // 忽略已存在记录,走下方的新建流程。 } } if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { diff --git a/internal/apps/upload/ingest/ingest_test.go b/internal/apps/upload/ingest/ingest_test.go index 5833938b..2fa30e2a 100644 --- a/internal/apps/upload/ingest/ingest_test.go +++ b/internal/apps/upload/ingest/ingest_test.go @@ -513,6 +513,7 @@ func loadTotalStats(ctx context.Context) (totalStatsSnapshot, error) { } func setupMockStorage(t *testing.T, putCount *int) (restore func(), disable func()) { + t.Helper() return setupMockStorageWithDeleteCount(t, putCount, nil) } diff --git a/internal/apps/upload/task/storage_migration.go b/internal/apps/upload/task/storage_migration.go index 616d2d56..9e2ea119 100644 --- a/internal/apps/upload/task/storage_migration.go +++ b/internal/apps/upload/task/storage_migration.go @@ -69,7 +69,7 @@ func (h *MigrationHandler) ValidatePayload(payload []byte) ([]byte, error) { return payload, err } if active { - return payload, fmt.Errorf("storage migration task is already unresolved") + return payload, errors.New("storage migration task is already unresolved") } return normalized, nil } @@ -193,18 +193,18 @@ func migrateObjects( const batchSize = 50 const migrationConcurrency = 10 const sha256HexLength = 64 - var migrated int64 + var migrated atomic.Int64 var lastFilePath string for { if err := ctx.Err(); err != nil { - return atomic.LoadInt64(&migrated), fmt.Errorf("storage migration canceled: %w", err) + return migrated.Load(), fmt.Errorf("storage migration canceled: %w", err) } - task.AppendLog(ctx, "正在查询待迁移对象批次,当前已完成迁移: %d/%d", atomic.LoadInt64(&migrated), total) + task.AppendLog(ctx, "正在查询待迁移对象批次,当前已完成迁移: %d/%d", migrated.Load(), total) objects, err := repository.ListDistinctActiveStorageObjects(ctx, lastFilePath, batchSize) if err != nil { - return atomic.LoadInt64(&migrated), fmt.Errorf("query source objects: %w", err) + return migrated.Load(), fmt.Errorf("query source objects: %w", err) } if len(objects) == 0 { task.AppendLog(ctx, "所有对象迁移完毕") @@ -223,18 +223,18 @@ func migrateObjects( if err := migrateSingleObject(ctx, sourceBackend, targetBackend, obj, sha256HexLength); err != nil { return err } - atomic.AddInt64(&migrated, 1) + migrated.Add(1) return nil }) } if err := g.Wait(); err != nil { - return atomic.LoadInt64(&migrated), err + return migrated.Load(), err } - task.AppendLog(ctx, "当前批次迁移完成。迁移进度: %d/%d", atomic.LoadInt64(&migrated), total) + task.AppendLog(ctx, "当前批次迁移完成。迁移进度: %d/%d", migrated.Load(), total) } - return atomic.LoadInt64(&migrated), nil + return migrated.Load(), nil } func migrateSingleObject( diff --git a/internal/apps/upload/util/media.go b/internal/apps/upload/util/media.go index eba2d864..c712afae 100644 --- a/internal/apps/upload/util/media.go +++ b/internal/apps/upload/util/media.go @@ -4,6 +4,7 @@ package util import ( + "slices" "strings" "github.com/Rain-kl/Wavelet/internal/apps/upload/shared" @@ -11,32 +12,17 @@ import ( // IsImageExtension reports whether ext is a common image format. func IsImageExtension(ext string) bool { - for _, imgExt := range []string{"jpg", "jpeg", "png", "webp", "gif"} { - if ext == imgExt { - return true - } - } - return false + return slices.Contains([]string{"jpg", "jpeg", "png", "webp", "gif"}, ext) } // IsArchiveExtension reports whether ext is a common archive format. func IsArchiveExtension(ext string) bool { - for _, e := range []string{"zip", "rar", "7z", "tar", "gz", "tgz", "bz2", "xz"} { - if ext == e { - return true - } - } - return false + return slices.Contains([]string{"zip", "rar", "7z", "tar", "gz", "tgz", "bz2", "xz"}, ext) } // IsDocumentExtension reports whether ext is a common document format. func IsDocumentExtension(ext string) bool { - for _, e := range []string{"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "md", "csv", "json", "yaml", "yml", "xml"} { - if ext == e { - return true - } - } - return false + return slices.Contains([]string{"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "md", "csv", "json", "yaml", "yml", "xml"}, ext) } // NormalizeImageQuality normalizes the requested image quality query parameter. diff --git a/internal/apps/user/tasks.go b/internal/apps/user/tasks.go index 167854a6..0849f326 100644 --- a/internal/apps/user/tasks.go +++ b/internal/apps/user/tasks.go @@ -152,7 +152,7 @@ func (h *SendEmailHandler) Execute(ctx context.Context, payload []byte) (*task.T return nil, fmt.Errorf(errSendMailFailed, err) } - msg := fmt.Sprintf("邮件成功发送至: %s", req.To) + msg := "邮件成功发送至: " + req.To task.AppendLog(ctx, "%s", msg) return &task.TaskResult{ diff --git a/internal/cmd/all.go b/internal/cmd/all.go index db7f0799..9da6e689 100644 --- a/internal/cmd/all.go +++ b/internal/cmd/all.go @@ -27,34 +27,28 @@ var allCmd = &cobra.Command{ var wg sync.WaitGroup // 启动 API HTTP 服务 - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { log.Println("[All] 启动 API 服务") router.Serve(func() { printStartupBanner(startupState{mode: "API + Worker + Scheduler", relationalDB: latestMigrationState.relationalDB, clickHouseDB: latestMigrationState.clickHouseDB, listensForHTTP: true}) }) - }() + }) // 启动 Asynq Worker 任务处理服务 - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { log.Println("[All] 启动 Worker 服务") if err := worker.StartWorker(); err != nil { log.Printf("[All] Worker 启动失败: %v\n", err) } - }() + }) // 启动 Asynq 定时任务调度器 - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { log.Println("[All] 启动 Scheduler 服务") if err := scheduler.StartScheduler(); err != nil { log.Printf("[All] Scheduler 启动失败: %v\n", err) } - }() + }) wg.Wait() }, diff --git a/internal/cmd/banner.go b/internal/cmd/banner.go index f079be73..ae354361 100644 --- a/internal/cmd/banner.go +++ b/internal/cmd/banner.go @@ -34,18 +34,18 @@ func formatStartupBanner(state startupState) string { "/ /_/ / /_/ / __/ / / / __/ / / /_/ / / / __/", "\\____/ .___/\\___/_/ /_/_/ /_/\\__,_/_/ \\___/ ", " /_/ ", - fmt.Sprintf(" OpenFlare %s", buildinfo.Version), + " OpenFlare " + buildinfo.Version, "", - fmt.Sprintf(" Environment: %s", config.Config.App.Env), + " Environment: " + config.Config.App.Env, fmt.Sprintf(" Runtime: %s/%s (%s)", runtime.GOOS, runtime.GOARCH, runtime.Version()), - fmt.Sprintf(" Build time: %s", buildTime()), - fmt.Sprintf(" Database: %s", formatMigration(state.relationalDB)), - fmt.Sprintf(" Analytics: %s", formatMigration(state.clickHouseDB)), + " Build time: " + buildTime(), + " Database: " + formatMigration(state.relationalDB), + " Analytics: " + formatMigration(state.clickHouseDB), } if state.listensForHTTP { - lines = append(lines, fmt.Sprintf(" Listening: http://%s", config.Config.App.Addr)) + lines = append(lines, " Listening: http://"+config.Config.App.Addr) } - lines = append(lines, fmt.Sprintf(" Mode: %s", state.mode), "") + lines = append(lines, " Mode: "+state.mode, "") return strings.Join(lines, "\n") } diff --git a/internal/infra/config/config.go b/internal/infra/config/config.go index 9a0b040a..be3a1cae 100644 --- a/internal/infra/config/config.go +++ b/internal/infra/config/config.go @@ -7,6 +7,7 @@ package config import ( "encoding/json" + "errors" "flag" "log" "os" @@ -31,10 +32,11 @@ func findConfigPath(configPath string) string { if _, err := os.Stat(configPath); err == nil { return configPath } - dir := "." - for i := 0; i < 5; i++ { - dir += "/.." - path := dir + "/" + configPath + var dir strings.Builder + dir.WriteString(".") + for range 5 { + dir.WriteString("/..") + path := dir.String() + "/" + configPath if _, err := os.Stat(path); err == nil { return path } @@ -68,7 +70,8 @@ func init() { // 读取配置文件(可选:找不到文件时使用空默认值 + 环境变量) if err := viper.ReadInConfig(); err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + var notFoundErr viper.ConfigFileNotFoundError + if !errors.As(err, ¬FoundErr) { // 文件存在但读取/解析失败 if _, statErr := os.Stat(configPath); statErr == nil { //nolint:gosec // configPath is loaded from CONFIG_PATH environment variable log.Fatalf("[Config] read config failed: %v\n", err) diff --git a/internal/infra/config/config_test.go b/internal/infra/config/config_test.go index e54f5a81..d353490f 100644 --- a/internal/infra/config/config_test.go +++ b/internal/infra/config/config_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package config import "testing" diff --git a/internal/infra/objectstore/local.go b/internal/infra/objectstore/local.go index 3e172116..0f02e5c8 100644 --- a/internal/infra/objectstore/local.go +++ b/internal/infra/objectstore/local.go @@ -5,6 +5,7 @@ package objectstore import ( "context" + "errors" "fmt" "io" "mime" @@ -20,7 +21,7 @@ type localBackend struct { func newLocalBackend(cfg LocalConfig) (*localBackend, error) { root := filepath.Clean(cfg.Root) if root == "" { - return nil, fmt.Errorf("local root is required") + return nil, errors.New("local root is required") } return &localBackend{root: root}, nil } @@ -103,7 +104,7 @@ func (b *localBackend) path(key string) (string, error) { } rel, err := filepath.Rel(absRoot, absPath) if err != nil || strings.HasPrefix(rel, "..") { - return "", fmt.Errorf("storage key escapes local root") + return "", errors.New("storage key escapes local root") } return cleanPath, nil } @@ -114,7 +115,7 @@ func (b *localBackend) path(key string) (string, error) { path := filepath.Join(b.root, cleanKey) rel, err := filepath.Rel(b.root, path) if err != nil || strings.HasPrefix(rel, "..") { - return "", fmt.Errorf("storage key escapes local root") + return "", errors.New("storage key escapes local root") } return path, nil } diff --git a/internal/infra/persistence/batchwriter/config.go b/internal/infra/persistence/batchwriter/config.go index f2363d16..cf19d708 100644 --- a/internal/infra/persistence/batchwriter/config.go +++ b/internal/infra/persistence/batchwriter/config.go @@ -4,7 +4,7 @@ package batchwriter import ( - "fmt" + "errors" "time" ) @@ -51,19 +51,19 @@ func DefaultConfig() Config { func (c Config) validate() error { if c.QueueSize <= 0 { - return fmt.Errorf("batchwriter: queue size must be positive") + return errors.New("batchwriter: queue size must be positive") } if c.MaxBatchSize <= 0 { - return fmt.Errorf("batchwriter: max batch size must be positive") + return errors.New("batchwriter: max batch size must be positive") } if c.MinBatchSize < 0 { - return fmt.Errorf("batchwriter: min batch size must be non-negative") + return errors.New("batchwriter: min batch size must be non-negative") } if c.FlushInterval <= 0 { - return fmt.Errorf("batchwriter: flush interval must be positive") + return errors.New("batchwriter: flush interval must be positive") } if c.MaxFlushWait < 0 { - return fmt.Errorf("batchwriter: max flush wait must be non-negative") + return errors.New("batchwriter: max flush wait must be non-negative") } return nil } diff --git a/internal/infra/persistence/migrator/pages_source_migration_test.go b/internal/infra/persistence/migrator/pages_source_migration_test.go index 8a40f68a..7bdbb577 100644 --- a/internal/infra/persistence/migrator/pages_source_migration_test.go +++ b/internal/infra/persistence/migrator/pages_source_migration_test.go @@ -198,7 +198,7 @@ func assertPagesSourceConstraints(t *testing.T, gormDB *gorm.DB, sourceID uint) ConfigVersion: 1, SourceIdentity: strings.Repeat("c", 64), } - assert.Error(t, gormDB.Create(&duplicateSource).Error) + require.Error(t, gormDB.Create(&duplicateSource).Error) for number := 2; number <= 3; number++ { require.NoError(t, createMigrationDeployment( @@ -219,14 +219,14 @@ func assertPagesSourceConstraints(t *testing.T, gormDB *gorm.DB, sourceID uint) &identity, &revision, )) - assert.Error(t, createMigrationDeployment( + require.Error(t, createMigrationDeployment( gormDB, 5, strings.Repeat("0", 64), &identity, &revision, )) - assert.Error(t, createMigrationDeployment( + require.Error(t, createMigrationDeployment( gormDB, 1, strings.Repeat("1", 64), diff --git a/internal/infra/persistence/postgres.go b/internal/infra/persistence/postgres.go index f21dbc91..78760855 100644 --- a/internal/infra/persistence/postgres.go +++ b/internal/infra/persistence/postgres.go @@ -21,6 +21,9 @@ import ( "gorm.io/plugin/opentelemetry/tracing" ) +// baseTracingOptionCount 是 newGORMTracingPlugin 预置的 tracing 选项数量。 +const baseTracingOptionCount = 3 + var ( db *gorm.DB ) @@ -106,7 +109,7 @@ func initPostgres() { } if len(dbConfig.Replicas) > 0 { - var replicaDialectors []gorm.Dialector + var replicaDialectors = make([]gorm.Dialector, 0, len(dbConfig.Replicas)) for _, replica := range dbConfig.Replicas { username := replica.Username if username == "" { @@ -156,11 +159,12 @@ func initPostgres() { // newGORMTracingPlugin 构造数据库链路追踪插件。查询参数只保留占位符,避免凭据等绑定值进入 Span。 func newGORMTracingPlugin(attrs []attribute.KeyValue, extraOptions ...tracing.Option) gorm.Plugin { - options := []tracing.Option{ + options := make([]tracing.Option, 0, baseTracingOptionCount+len(extraOptions)) + options = append(options, tracing.WithoutMetrics(), tracing.WithoutQueryVariables(), tracing.WithAttributes(attrs...), - } + ) options = append(options, extraOptions...) return tracing.NewPlugin(options...) } diff --git a/internal/infra/persistence/postgres_logger.go b/internal/infra/persistence/postgres_logger.go index 2ba1433c..d7398daf 100644 --- a/internal/infra/persistence/postgres_logger.go +++ b/internal/infra/persistence/postgres_logger.go @@ -31,26 +31,26 @@ func (l *gormZapLogger) LogMode(level gormLogger.LogLevel) gormLogger.Interface return &clone } -func (l *gormZapLogger) Info(ctx context.Context, fmt string, args ...interface{}) { +func (l *gormZapLogger) Info(ctx context.Context, fmt string, args ...any) { if l.logLevel >= gormLogger.Info { logger.InfoF(ctx, fmt, args...) } } -func (l *gormZapLogger) Warn(ctx context.Context, fmt string, args ...interface{}) { +func (l *gormZapLogger) Warn(ctx context.Context, fmt string, args ...any) { if l.logLevel >= gormLogger.Warn { logger.WarnF(ctx, fmt, args...) } } -func (l *gormZapLogger) Error(ctx context.Context, fmt string, args ...interface{}) { +func (l *gormZapLogger) Error(ctx context.Context, fmt string, args ...any) { if l.logLevel >= gormLogger.Error { logger.ErrorF(ctx, fmt, args...) } } // ParamsFilter 让 GORM 的 Trace 回调只接收参数化 SQL,避免绑定值被 Dialector.Explain 展开到日志。 -func (l *gormZapLogger) ParamsFilter(_ context.Context, sql string, _ ...interface{}) (string, []interface{}) { +func (l *gormZapLogger) ParamsFilter(_ context.Context, sql string, _ ...any) (string, []any) { return sql, nil } @@ -70,7 +70,7 @@ func (l *gormZapLogger) Trace(ctx context.Context, begin time.Time, fc func() (s } } -func formatRows(rows int64) interface{} { +func formatRows(rows int64) any { if rows == -1 { return "-" } diff --git a/internal/infra/persistence/redis.go b/internal/infra/persistence/redis.go index 541ae165..78f3e28b 100644 --- a/internal/infra/persistence/redis.go +++ b/internal/infra/persistence/redis.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "log" + "strconv" "strings" "time" @@ -83,7 +84,7 @@ func init() { if err := redisotel.InstrumentTracing( Redis, redisotel.WithAttributes( - attribute.String("db.instance", fmt.Sprintf("%v", cfg.DB)), + attribute.String("db.instance", strconv.Itoa(cfg.DB)), attribute.String("db.ip", strings.Join(cfg.Addrs, ",")), attribute.String("db.system", "Redis"), ), diff --git a/internal/infra/persistence/redis_test.go b/internal/infra/persistence/redis_test.go index ace2e50e..55ec55e2 100644 --- a/internal/infra/persistence/redis_test.go +++ b/internal/infra/persistence/redis_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package db import ( diff --git a/internal/infra/task/executor.go b/internal/infra/task/executor.go index c8dfac41..89ef029f 100644 --- a/internal/infra/task/executor.go +++ b/internal/infra/task/executor.go @@ -103,7 +103,7 @@ func IsFinalAttempt(ctx context.Context) bool { // AppendLog 追加日志到任务执行记录 // 在 TaskHandler.Execute 中调用,日志会自动追加到 TaskExecution.Log 字段 -func AppendLog(ctx context.Context, format string, args ...interface{}) { +func AppendLog(ctx context.Context, format string, args ...any) { taskID := GetTaskID(ctx) if taskID == "" { // 上下文中没有 taskID,降级到普通日志 diff --git a/internal/infra/task/executor_test.go b/internal/infra/task/executor_test.go index a58adc2b..44efc23e 100644 --- a/internal/infra/task/executor_test.go +++ b/internal/infra/task/executor_test.go @@ -59,6 +59,7 @@ func failHandler() *mockHandler { const testTaskType = "test:mock_task" func setupTest(t *testing.T) func() { + t.Helper() _, mr, cleanup := testhelper.SetupTestEnvironment(t) AsynqClient = asynq.NewClient(asynq.RedisClientOpt{ Addr: mr.Addr(), @@ -94,7 +95,7 @@ func TestGetTaskIDFromContext(t *testing.T) { // 空 context taskID := GetTaskID(ctx) - assert.Equal(t, "", taskID) + assert.Empty(t, taskID) // 注入 taskID ctx = withTaskID(ctx, "test_task_123") @@ -242,7 +243,7 @@ func TestProcessTaskFailure(t *testing.T) { ctx = withTaskID(ctx, "process_fail_001") _, err = handler.Execute(ctx, nil) - assert.Error(t, err) + require.Error(t, err) assert.Contains(t, err.Error(), "模拟执行失败") // 验证日志 @@ -403,7 +404,7 @@ func TestRetryTaskNotFailed(t *testing.T) { // 尝试重试成功的任务 _, err = RetryTask(ctx, execution.ID) - assert.Error(t, err) + require.Error(t, err) assert.Contains(t, err.Error(), "只有失败的任务才能重试") } @@ -425,7 +426,7 @@ func TestRetryTaskNotRetryable(t *testing.T) { require.NoError(t, err) _, err = RetryTask(ctx, execution.ID) - assert.Error(t, err) + require.Error(t, err) assert.Contains(t, err.Error(), "不支持重试") } @@ -435,7 +436,7 @@ func TestRetryTaskNonExistent(t *testing.T) { ctx := context.Background() _, err := RetryTask(ctx, 99999999) - assert.Error(t, err) + require.Error(t, err) assert.Contains(t, err.Error(), "不存在") } diff --git a/internal/infra/task/permanent_error_test.go b/internal/infra/task/permanent_error_test.go index f8ae9300..d90c3e8c 100644 --- a/internal/infra/task/permanent_error_test.go +++ b/internal/infra/task/permanent_error_test.go @@ -4,17 +4,17 @@ package task import ( - "errors" "testing" "github.com/hibiken/asynq" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPermanentErrorSkipsRetryWithoutExposingAsynqMessage(t *testing.T) { err := PermanentError(" 来源配置无效 ") - assert.True(t, errors.Is(err, asynq.SkipRetry)) + require.ErrorIs(t, err, asynq.SkipRetry) assert.Equal(t, "来源配置无效", err.Error()) assert.NotContains(t, err.Error(), asynq.SkipRetry.Error()) } @@ -22,6 +22,6 @@ func TestPermanentErrorSkipsRetryWithoutExposingAsynqMessage(t *testing.T) { func TestPermanentErrorUsesSafeFallbackForBlankMessage(t *testing.T) { err := PermanentError(" ") - assert.True(t, errors.Is(err, asynq.SkipRetry)) - assert.Equal(t, defaultPermanentErrorMessage, err.Error()) + require.ErrorIs(t, err, asynq.SkipRetry) + require.Equal(t, defaultPermanentErrorMessage, err.Error()) } diff --git a/internal/infra/task/utils.go b/internal/infra/task/utils.go index d5afa098..5c409aae 100644 --- a/internal/infra/task/utils.go +++ b/internal/infra/task/utils.go @@ -15,7 +15,7 @@ type redisClientConnOpt struct { options redis.Options } -func (opt redisClientConnOpt) MakeRedisClient() interface{} { +func (opt redisClientConnOpt) MakeRedisClient() any { return redis.NewClient(&opt.options) } @@ -23,7 +23,7 @@ type redisClusterConnOpt struct { options redis.ClusterOptions } -func (opt redisClusterConnOpt) MakeRedisClient() interface{} { +func (opt redisClusterConnOpt) MakeRedisClient() any { return redis.NewClusterClient(&opt.options) } @@ -32,7 +32,7 @@ type redisFailoverConnOpt struct { maintNotificationsEnabled bool } -func (opt redisFailoverConnOpt) MakeRedisClient() interface{} { +func (opt redisFailoverConnOpt) MakeRedisClient() any { client := redis.NewFailoverClient(&opt.options) // go-redis v9.16 does not expose maintenance notification settings on // FailoverOptions, so apply the configured mode before the client is used. diff --git a/internal/infra/task/utils_test.go b/internal/infra/task/utils_test.go index aae01e32..b16ab71d 100644 --- a/internal/infra/task/utils_test.go +++ b/internal/infra/task/utils_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package task import ( diff --git a/internal/integration/githubrelease/client.go b/internal/integration/githubrelease/client.go index 83373ea8..0195002d 100644 --- a/internal/integration/githubrelease/client.go +++ b/internal/integration/githubrelease/client.go @@ -100,7 +100,7 @@ type Release struct { Name string `json:"name,omitempty"` Draft bool `json:"draft"` Prerelease bool `json:"prerelease"` - PublishedAt time.Time `json:"published_at,omitempty"` + PublishedAt time.Time `json:"published_at,omitzero"` } // Asset contains the immutable target metadata returned by a resolve call. @@ -109,7 +109,7 @@ type Asset struct { Name string `json:"asset_name"` State string `json:"state"` Size int64 `json:"size"` - UpdatedAt time.Time `json:"updated_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitzero"` Digest string `json:"digest,omitempty"` } @@ -338,7 +338,7 @@ func (client *Client) Resolve(ctx context.Context, request ResolveRequest) (Reso return ResolveResult{}, safeError( errAssetMissing, response.StatusCode, - response.Header.Get("X-GitHub-Request-Id"), + response.Header.Get("X-Github-Request-Id"), repository, release.Tag, request.AssetName, @@ -379,7 +379,7 @@ func (client *Client) Download(ctx context.Context, request DownloadRequest) (*D result, err := client.streamAsset(response.Body, request.MaxBytes, request.Asset.Digest) if err != nil { - return nil, safeError(err, response.StatusCode, response.Header.Get("X-GitHub-Request-Id"), repository, "", request.Asset.Name, nil, retryAt) + return nil, safeError(err, response.StatusCode, response.Header.Get("X-Github-Request-Id"), repository, "", request.Asset.Name, nil, retryAt) } return result, nil } @@ -533,7 +533,7 @@ func validTag(tag string) bool { strings.HasPrefix(tag, "/") || strings.HasSuffix(tag, "/") || strings.HasSuffix(tag, ".") { return false } - for _, component := range strings.Split(tag, "/") { + for component := range strings.SplitSeq(tag, "/") { if component == "" || strings.HasPrefix(component, ".") || strings.HasSuffix(component, ".lock") { return false } @@ -635,10 +635,7 @@ func verifyDeclaredDigest(declaredDigest string, checksum string) error { } func safeAssetNames(assets []Asset) []string { - count := len(assets) - if count > maxAssetErrorNames { - count = maxAssetErrorNames - } + count := min(len(assets), maxAssetErrorNames) names := make([]string, 0, count) for _, asset := range assets[:count] { name := safeText(asset.Name, maxSafeAssetNameLen) @@ -682,7 +679,7 @@ func safeHTTPError(kind error, response *http.Response, repository string, tag s return safeError( kind, response.StatusCode, - response.Header.Get("X-GitHub-Request-Id"), + response.Header.Get("X-Github-Request-Id"), repository, tag, assetName, diff --git a/internal/integration/githubrelease/transport.go b/internal/integration/githubrelease/transport.go index 16a82471..7ea75e4e 100644 --- a/internal/integration/githubrelease/transport.go +++ b/internal/integration/githubrelease/transport.go @@ -148,7 +148,7 @@ func newClient(options clientOptions) *Client { func applyMetadataHeaders(request *http.Request, etag string) { request.Header.Set("Accept", metadataAccept) request.Header.Set("User-Agent", defaultUserAgent) - request.Header.Set("X-GitHub-Api-Version", APIVersion) + request.Header.Set("X-Github-Api-Version", APIVersion) if etag = safeETag(etag); etag != "" { request.Header.Set("If-None-Match", etag) } @@ -158,7 +158,7 @@ func applyAssetHeaders(request *http.Request) { request.Header.Set("Accept", assetAccept) request.Header.Set("Accept-Encoding", "identity") request.Header.Set("User-Agent", defaultUserAgent) - request.Header.Set("X-GitHub-Api-Version", APIVersion) + request.Header.Set("X-Github-Api-Version", APIVersion) } func stripCrossHostHeaders(request *http.Request) { @@ -300,10 +300,10 @@ func responseRetryAt(response *http.Response, now time.Time) *time.Time { return &retryAt } } - if strings.TrimSpace(response.Header.Get("X-RateLimit-Remaining")) != "0" { + if strings.TrimSpace(response.Header.Get("X-Ratelimit-Remaining")) != "0" { return nil } - reset, err := strconv.ParseInt(strings.TrimSpace(response.Header.Get("X-RateLimit-Reset")), 10, 64) + reset, err := strconv.ParseInt(strings.TrimSpace(response.Header.Get("X-Ratelimit-Reset")), 10, 64) if err != nil || reset <= 0 { return nil } diff --git a/internal/model/access_token.go b/internal/model/access_token.go index 91993a25..7665f3ed 100644 --- a/internal/model/access_token.go +++ b/internal/model/access_token.go @@ -41,7 +41,7 @@ func GenerateTokenString() (string, error) { if _, err := rand.Read(bytes); err != nil { return "", err } - return fmt.Sprintf("at_%s", hex.EncodeToString(bytes)), nil + return "at_" + hex.EncodeToString(bytes), nil } // HashToken 计算 Token 的 SHA-256 哈希值用于数据库存储与查询 diff --git a/internal/model/auth_source.go b/internal/model/auth_source.go index 00bc4144..4a63031e 100644 --- a/internal/model/auth_source.go +++ b/internal/model/auth_source.go @@ -43,7 +43,7 @@ type AuthSource struct { } // TableName 表名 -func (AuthSource) TableName() string { +func (*AuthSource) TableName() string { return "w_auth_sources" } diff --git a/internal/model/openflare_config_version.go b/internal/model/openflare_config_version.go index d1f33fd1..cae816d3 100644 --- a/internal/model/openflare_config_version.go +++ b/internal/model/openflare_config_version.go @@ -52,6 +52,6 @@ func (cv *ConfigVersion) AfterCreate(_ *gorm.DB) (err error) { } // TableName returns the GORM table name. -func (ConfigVersion) TableName() string { +func (*ConfigVersion) TableName() string { return "of_config_versions" } diff --git a/internal/model/push_channel.go b/internal/model/push_channel.go index b9217e64..111aa06d 100644 --- a/internal/model/push_channel.go +++ b/internal/model/push_channel.go @@ -35,7 +35,7 @@ type PushChannel struct { } // TableName 指定 GORM 表名 -func (PushChannel) TableName() string { +func (*PushChannel) TableName() string { return "w_push_channels" } diff --git a/internal/model/push_event.go b/internal/model/push_event.go index 1307d30b..e64add1a 100644 --- a/internal/model/push_event.go +++ b/internal/model/push_event.go @@ -24,7 +24,7 @@ type PushEvent struct { } // TableName 指定 GORM 表名 -func (PushEvent) TableName() string { +func (*PushEvent) TableName() string { return "w_push_events" } diff --git a/internal/model/templates.go b/internal/model/templates.go index 28f78ad1..e39f7e3a 100644 --- a/internal/model/templates.go +++ b/internal/model/templates.go @@ -26,7 +26,7 @@ type Template struct { } // TableName 表名 -func (Template) TableName() string { +func (*Template) TableName() string { return "w_templates" } diff --git a/internal/model/users.go b/internal/model/users.go index 2a717f06..9c61bf2e 100644 --- a/internal/model/users.go +++ b/internal/model/users.go @@ -61,7 +61,7 @@ type User struct { } // TableName 表名 -func (User) TableName() string { +func (*User) TableName() string { return "w_users" } diff --git a/internal/repository/analytics/access_log.go b/internal/repository/analytics/access_log.go index 045f03a9..9be348ec 100644 --- a/internal/repository/analytics/access_log.go +++ b/internal/repository/analytics/access_log.go @@ -6,6 +6,7 @@ package analytics import ( "context" + "errors" "fmt" "time" @@ -15,7 +16,7 @@ import ( func userAccessLogConn() error { if db.ChConn == nil { - return fmt.Errorf("clickhouse native connection is not initialized") + return errors.New("clickhouse native connection is not initialized") } return nil } diff --git a/internal/repository/analytics/access_log_stats.go b/internal/repository/analytics/access_log_stats.go index 9527ec87..6192e843 100644 --- a/internal/repository/analytics/access_log_stats.go +++ b/internal/repository/analytics/access_log_stats.go @@ -50,7 +50,7 @@ func GetDailyTrend(ctx context.Context, days int) ([]DailyTrend, error) { defer func() { _ = rows.Close() }() trendMap := make(map[string]uint64, days) - for i := 0; i < days; i++ { + for i := range days { dateStr := time.Now().AddDate(0, 0, -i).Format("2006-01-02") trendMap[dateStr] = 0 } diff --git a/internal/repository/analytics/access_log_writer.go b/internal/repository/analytics/access_log_writer.go index 337ca966..d79627ee 100644 --- a/internal/repository/analytics/access_log_writer.go +++ b/internal/repository/analytics/access_log_writer.go @@ -5,6 +5,7 @@ package analytics import ( "context" + "errors" "fmt" db "github.com/Rain-kl/Wavelet/internal/infra/persistence" @@ -17,7 +18,7 @@ func BatchInsert(ctx context.Context, logs []analyticsmodel.UserAccessLog) error return nil } if db.ChConn == nil { - return fmt.Errorf("clickhouse connection is not initialized") + return errors.New("clickhouse connection is not initialized") } batch, err := db.ChConn.PrepareBatch(ctx, analyticsmodel.UserAccessLog{}.BatchInsertSQL()) diff --git a/internal/repository/analytics/clickhouse_stats.go b/internal/repository/analytics/clickhouse_stats.go index c6547902..7a4a71de 100644 --- a/internal/repository/analytics/clickhouse_stats.go +++ b/internal/repository/analytics/clickhouse_stats.go @@ -5,6 +5,7 @@ package analytics import ( "context" + "errors" "fmt" "github.com/Rain-kl/Wavelet/internal/infra/config" @@ -19,7 +20,7 @@ type ClickHouseOperationalStats = analyticsmodel.ClickHouseOperationalStats // GetClickHouseOperationalStats returns operational metrics for the configured database. func GetClickHouseOperationalStats(ctx context.Context) (*ClickHouseOperationalStats, error) { if db.ChConn == nil { - return nil, fmt.Errorf("clickhouse native connection is not initialized") + return nil, errors.New("clickhouse native connection is not initialized") } database := config.Config.ClickHouse.Database stats := &ClickHouseOperationalStats{Database: database} diff --git a/internal/repository/analytics/node_access_log.go b/internal/repository/analytics/node_access_log.go index 38b184db..a8e78f6c 100644 --- a/internal/repository/analytics/node_access_log.go +++ b/internal/repository/analytics/node_access_log.go @@ -5,6 +5,7 @@ package analytics import ( "context" + "errors" "fmt" "strings" "time" @@ -19,7 +20,7 @@ type NodeAccessLogRegionCount = analyticsmodel.NodeAccessLogRegionCount func nodeAccessLogConn() (driver.Conn, error) { if db.ChConn == nil { - return nil, fmt.Errorf("clickhouse connection is not initialized") + return nil, errors.New("clickhouse connection is not initialized") } return db.ChConn, nil } diff --git a/internal/repository/analytics/node_access_log_test.go b/internal/repository/analytics/node_access_log_test.go index 61246452..5bc75c6f 100644 --- a/internal/repository/analytics/node_access_log_test.go +++ b/internal/repository/analytics/node_access_log_test.go @@ -50,8 +50,8 @@ func TestBatchInsertNodeAccessLogs_UsesModelBatchSQL(t *testing.T) { require.Len(t, mockBatch.rows, 1) assert.Equal(t, "node-a", mockBatch.rows[0][1]) require.Len(t, mockBatch.rows[0], 14) - assert.Equal(t, "", mockBatch.rows[0][7]) // user_agent - assert.Equal(t, "", mockBatch.rows[0][8]) // cache_status + assert.Empty(t, mockBatch.rows[0][7]) // user_agent + assert.Empty(t, mockBatch.rows[0][8]) // cache_status assert.Equal(t, uint64(2048), mockBatch.rows[0][10]) // bytes_sent assert.Equal(t, uint64(0), mockBatch.rows[0][11]) // request_length assert.Equal(t, uint32(0), mockBatch.rows[0][12]) // request_time_ms diff --git a/internal/repository/analytics/node_access_log_writer.go b/internal/repository/analytics/node_access_log_writer.go index 062de249..0558c109 100644 --- a/internal/repository/analytics/node_access_log_writer.go +++ b/internal/repository/analytics/node_access_log_writer.go @@ -5,6 +5,7 @@ package analytics import ( "context" + "errors" "fmt" "strings" "time" @@ -20,7 +21,7 @@ func BatchInsertNodeAccessLogs(ctx context.Context, logs []analyticsmodel.NodeAc return nil } if db.ChConn == nil { - return fmt.Errorf("clickhouse connection is not initialized") + return errors.New("clickhouse connection is not initialized") } batch, err := db.ChConn.PrepareBatch(ctx, analyticsmodel.NodeAccessLog{}.BatchInsertSQL()) diff --git a/internal/repository/analytics/node_observability.go b/internal/repository/analytics/node_observability.go index 2c9ec39c..42668976 100644 --- a/internal/repository/analytics/node_observability.go +++ b/internal/repository/analytics/node_observability.go @@ -5,8 +5,9 @@ package analytics import ( "context" + "errors" "fmt" - "sort" + "slices" "time" "github.com/ClickHouse/clickhouse-go/v2/lib/driver" @@ -16,7 +17,7 @@ import ( func observabilityConn() (driver.Conn, error) { if db.ChConn == nil { - return nil, fmt.Errorf("clickhouse connection is not initialized") + return nil, errors.New("clickhouse connection is not initialized") } return db.ChConn, nil } @@ -367,7 +368,7 @@ func mergeNodeMetricHourlyPreferRollup(rollup, raw []NodeMetricHourly) []NodeMet } result := make([]NodeMetricHourly, 0, len(order)) // Keep chronological order of first-seen keys; re-sort by hour for stability. - sort.Slice(order, func(i, j int) bool { return order[i] < order[j] }) + slices.Sort(order) for _, key := range order { result = append(result, byHour[key]) } diff --git a/internal/repository/analytics/node_observability_latest_test.go b/internal/repository/analytics/node_observability_latest_test.go index dfebce46..f78e4ae2 100644 --- a/internal/repository/analytics/node_observability_latest_test.go +++ b/internal/repository/analytics/node_observability_latest_test.go @@ -55,8 +55,8 @@ func TestListNodeMetricHourly_PrefersRollup(t *testing.T) { rows, err := ListNodeMetricHourly(ctx, NodeObservabilityFilter{Since: since}) require.NoError(t, err) require.Len(t, rows, 1) - assert.Equal(t, 42.5, rows[0].AverageCPUUsagePercent) - assert.Equal(t, 60.0, rows[0].AverageMemoryUsagePercent) + assert.InDelta(t, 42.5, rows[0].AverageCPUUsagePercent, 1e-9) + assert.InDelta(t, 60.0, rows[0].AverageMemoryUsagePercent, 1e-9) assert.Equal(t, int64(100), rows[0].NetworkRxBytes) assert.Equal(t, 2, rows[0].ReportedNodes) require.Len(t, mock.queries, 1) @@ -92,10 +92,10 @@ func TestListNodeMetricHourly_MergesRawGapsWithPartialRollup(t *testing.T) { require.NoError(t, err) require.Len(t, rows, 2) assert.Equal(t, rawHour, rows[0].Hour) - assert.Equal(t, 12.0, rows[0].AverageCPUUsagePercent) + assert.InDelta(t, 12.0, rows[0].AverageCPUUsagePercent, 1e-9) // Overlapping hour prefers rollup (99) over raw (50). assert.Equal(t, rollupHour, rows[1].Hour) - assert.Equal(t, 99.0, rows[1].AverageCPUUsagePercent) + assert.InDelta(t, 99.0, rows[1].AverageCPUUsagePercent, 1e-9) require.GreaterOrEqual(t, len(mock.queries), 2) assert.Contains(t, mock.queries[1], "lagInFrame") } @@ -112,9 +112,9 @@ func TestMergeNodeMetricHourlyPreferRollup(t *testing.T) { ) require.Len(t, merged, 2) assert.Equal(t, h1, merged[0].Hour) - assert.Equal(t, 10.0, merged[0].AverageCPUUsagePercent) + assert.InDelta(t, 10.0, merged[0].AverageCPUUsagePercent, 1e-9) assert.Equal(t, h2, merged[1].Hour) - assert.Equal(t, 80.0, merged[1].AverageCPUUsagePercent) + assert.InDelta(t, 80.0, merged[1].AverageCPUUsagePercent, 1e-9) } func TestHourlyRollupCoversWindow(t *testing.T) { @@ -147,7 +147,7 @@ func TestListNodeMetricHourly_FallsBackToRawOnRollupError(t *testing.T) { rows, err := ListNodeMetricHourly(ctx, NodeObservabilityFilter{}) require.NoError(t, err) require.Len(t, rows, 1) - assert.Equal(t, 10.0, rows[0].AverageCPUUsagePercent) + assert.InDelta(t, 10.0, rows[0].AverageCPUUsagePercent, 1e-9) assert.Equal(t, int64(3), rows[0].DiskReadBytes) require.GreaterOrEqual(t, len(mock.queries), 2) assert.Contains(t, mock.queries[0], nodeMetricCapacityHourlyTableName()) diff --git a/internal/repository/analytics/node_observability_writer.go b/internal/repository/analytics/node_observability_writer.go index a45673d5..6deff8c4 100644 --- a/internal/repository/analytics/node_observability_writer.go +++ b/internal/repository/analytics/node_observability_writer.go @@ -5,6 +5,7 @@ package analytics import ( "context" + "errors" "fmt" "strings" "time" @@ -30,7 +31,7 @@ func BatchInsertNodeMetricSnapshots(ctx context.Context, snapshots []analyticsmo return nil } if db.ChConn == nil { - return fmt.Errorf("clickhouse connection is not initialized") + return errors.New("clickhouse connection is not initialized") } batch, err := db.ChConn.PrepareBatch(ctx, analyticsmodel.NodeMetricSnapshot{}.BatchInsertSQL()) @@ -102,7 +103,7 @@ func BatchInsertNodeEdgeHealth(ctx context.Context, rows []analyticsmodel.NodeEd return nil } if db.ChConn == nil { - return fmt.Errorf("clickhouse connection is not initialized") + return errors.New("clickhouse connection is not initialized") } batch, err := db.ChConn.PrepareBatch(ctx, analyticsmodel.NodeEdgeHealth{}.BatchInsertSQL()) if err != nil { @@ -160,7 +161,7 @@ func BatchInsertNodeObsFrps(ctx context.Context, observations []analyticsmodel.N return nil } if db.ChConn == nil { - return fmt.Errorf("clickhouse connection is not initialized") + return errors.New("clickhouse connection is not initialized") } batch, err := db.ChConn.PrepareBatch(ctx, analyticsmodel.NodeObsFrps{}.BatchInsertSQL()) @@ -223,7 +224,7 @@ func BatchInsertNodeObsFrpc(ctx context.Context, observations []analyticsmodel.N return nil } if db.ChConn == nil { - return fmt.Errorf("clickhouse connection is not initialized") + return errors.New("clickhouse connection is not initialized") } batch, err := db.ChConn.PrepareBatch(ctx, analyticsmodel.NodeObsFrpc{}.BatchInsertSQL()) diff --git a/internal/repository/auth_source_cache.go b/internal/repository/auth_source_cache.go index 2dd4aa7d..c075e83f 100644 --- a/internal/repository/auth_source_cache.go +++ b/internal/repository/auth_source_cache.go @@ -48,6 +48,7 @@ var ( authSourceListenerOnce sync.Once authSourceListenerCtx context.Context authSourceListenerCancel context.CancelFunc + authSourceListenerDone chan struct{} ) func cloneAuthSources(sources []model.AuthSource) []model.AuthSource { @@ -116,15 +117,19 @@ func ensureAuthSourceCacheListener() { func startAuthSourceCacheInvalidationListener() { authSourceListenerCtx, authSourceListenerCancel = context.WithCancel(context.Background()) + authSourceListenerDone = make(chan struct{}) go func() { - pubsub := db.Redis.Subscribe(authSourceListenerCtx, authSourceInvalidationChannel) + listenerCtx := authSourceListenerCtx + defer close(authSourceListenerDone) + + pubsub := db.Redis.Subscribe(listenerCtx, authSourceInvalidationChannel) defer func() { _ = pubsub.Close() }() go func() { - <-authSourceListenerCtx.Done() + <-listenerCtx.Done() _ = pubsub.Close() }() @@ -257,7 +262,11 @@ func InvalidateAuthSourceCache(ctx context.Context) error { func StopAuthSourceCacheListener() { if authSourceListenerCancel != nil { authSourceListenerCancel() + if authSourceListenerDone != nil { + <-authSourceListenerDone + } authSourceListenerCancel = nil + authSourceListenerDone = nil } authSourceListenerOnce = sync.Once{} } diff --git a/internal/repository/logstore/postgres_store.go b/internal/repository/logstore/postgres_store.go index 0ae1bdcf..338b10d7 100644 --- a/internal/repository/logstore/postgres_store.go +++ b/internal/repository/logstore/postgres_store.go @@ -1536,7 +1536,7 @@ func (s *userAccessLogGormStore) GetDailyTrend(ctx context.Context, days int) ([ counts[r.Date] = r.Cnt } out := make([]analyticsmodel.DailyTrend, 0, days) - for i := 0; i < days; i++ { + for i := range days { d := start.AddDate(0, 0, i).Format("2006-01-02") out = append(out, analyticsmodel.DailyTrend{Date: d, Count: counts[d]}) } diff --git a/internal/repository/openflare_access_log.go b/internal/repository/openflare_access_log.go index b5ac1ca1..0c6d177a 100644 --- a/internal/repository/openflare_access_log.go +++ b/internal/repository/openflare_access_log.go @@ -418,14 +418,8 @@ func openFlareAccessLogPaginateBounds(total int, page int, pageSize int) (int, i if pageSize <= 0 { return 0, total } - start := page * pageSize - if start > total { - start = total - } - end := start + pageSize - if end > total { - end = total - } + start := min(page*pageSize, total) + end := min(start+pageSize, total) return start, end } diff --git a/internal/repository/openflare_apply_log_test.go b/internal/repository/openflare_apply_log_test.go index 9408f9cd..224716de 100644 --- a/internal/repository/openflare_apply_log_test.go +++ b/internal/repository/openflare_apply_log_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package repository import ( diff --git a/internal/repository/openflare_zone.go b/internal/repository/openflare_zone.go index 9f99c877..e73aa42c 100644 --- a/internal/repository/openflare_zone.go +++ b/internal/repository/openflare_zone.go @@ -1,9 +1,11 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package repository import ( "context" "errors" - "fmt" "gorm.io/gorm" @@ -124,7 +126,7 @@ func ListZoneDomainsByIDs(ctx context.Context, domainIDs []uint) ([]model.ZoneDo for _, id := range domainIDs { domain, ok := byID[id] if !ok { - return nil, fmt.Errorf("one or more zone domains do not exist") + return nil, errors.New("one or more zone domains do not exist") } ordered = append(ordered, domain) } diff --git a/internal/repository/system_config_cache.go b/internal/repository/system_config_cache.go index 7a620461..956d2559 100644 --- a/internal/repository/system_config_cache.go +++ b/internal/repository/system_config_cache.go @@ -90,6 +90,7 @@ var ( systemConfigListenerOnce sync.Once systemConfigListenerCtx context.Context systemConfigListenerCancel context.CancelFunc + systemConfigListenerDone chan struct{} ) func ensureSystemConfigCacheListener() { @@ -102,15 +103,19 @@ func startSystemConfigCacheInvalidationListener() { } systemConfigListenerCtx, systemConfigListenerCancel = context.WithCancel(context.Background()) + systemConfigListenerDone = make(chan struct{}) go func() { - pubsub := db.Redis.Subscribe(systemConfigListenerCtx, SystemConfigBroadcastChannel) + listenerCtx := systemConfigListenerCtx + defer close(systemConfigListenerDone) + + pubsub := db.Redis.Subscribe(listenerCtx, SystemConfigBroadcastChannel) defer func() { _ = pubsub.Close() }() go func() { - <-systemConfigListenerCtx.Done() + <-listenerCtx.Done() _ = pubsub.Close() }() @@ -135,7 +140,11 @@ func startSystemConfigCacheInvalidationListener() { func StopSystemConfigCacheListener() { if systemConfigListenerCancel != nil { systemConfigListenerCancel() + if systemConfigListenerDone != nil { + <-systemConfigListenerDone + } systemConfigListenerCancel = nil + systemConfigListenerDone = nil } systemConfigListenerOnce = sync.Once{} } diff --git a/internal/repository/task_execution.go b/internal/repository/task_execution.go index 8d60d398..81284f2c 100644 --- a/internal/repository/task_execution.go +++ b/internal/repository/task_execution.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package repository import ( diff --git a/internal/repository/task_execution_test.go b/internal/repository/task_execution_test.go index 9d169be2..f56da601 100644 --- a/internal/repository/task_execution_test.go +++ b/internal/repository/task_execution_test.go @@ -23,6 +23,7 @@ import ( ) func setupTaskExecutionTestEnvironment(t *testing.T) func() { + t.Helper() sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ DisableForeignKeyConstraintWhenMigrating: true, }) @@ -323,7 +324,7 @@ func TestAppendTaskExecutionLogNonExistent(t *testing.T) { // Redis 缓冲不依赖数据库记录是否已经创建。 err := AppendTaskExecutionLog(ctx, "nonexistent_task", "测试日志") - assert.NoError(t, err) + require.NoError(t, err) err = FlushTaskExecutionLog(ctx, "nonexistent_task") assert.Error(t, err) @@ -454,7 +455,7 @@ func TestListTaskExecutionsDefaultPaging(t *testing.T) { items, total, err := ListTaskExecutions(ctx, model.ListTaskExecutionsRequest{}) require.NoError(t, err) assert.Equal(t, int64(0), total) - assert.Len(t, items, 0) + assert.Empty(t, items) } func TestCleanupTaskExecutionLogs(t *testing.T) { diff --git a/internal/router/middlewares.go b/internal/router/middlewares.go index 58a2389b..ceb9e874 100644 --- a/internal/router/middlewares.go +++ b/internal/router/middlewares.go @@ -63,7 +63,7 @@ func loggerMiddleware() gin.HandlerFunc { func logRequest(ctx context.Context, c *gin.Context, path string, start, end time.Time, latency time.Duration) { format := "[LoggerMiddleware] %s %s\nStartTime: %s\nEndTime: %s\nLatency: %d\nClientIP: %s\nResponse: %d %d" - args := []interface{}{ + args := []any{ c.Request.Method, path, start.Format(time.RFC3339), @@ -89,8 +89,8 @@ func isOriginAllowed(ctx context.Context, origin string) bool { if err != nil || sc.Value == "" { return false } - allowedOrigins := strings.Split(sc.Value, ",") - for _, allowed := range allowedOrigins { + allowedOrigins := strings.SplitSeq(sc.Value, ",") + for allowed := range allowedOrigins { allowed = strings.TrimRight(strings.TrimSpace(allowed), "/") if allowed != "" && strings.EqualFold(allowed, origin) { return true @@ -109,7 +109,7 @@ func corsMiddleware() gin.HandlerFunc { c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH") } - if c.Request.Method == "OPTIONS" { + if c.Request.Method == http.MethodOptions { c.AbortWithStatus(http.StatusNoContent) return } diff --git a/internal/shared/response/middleware_test.go b/internal/shared/response/middleware_test.go index 0a354973..d593f7cc 100644 --- a/internal/shared/response/middleware_test.go +++ b/internal/shared/response/middleware_test.go @@ -34,7 +34,7 @@ func TestAbortWithError(t *testing.T) { require.Len(t, c.Errors, 1) var apiErr *APIError - require.True(t, errors.As(c.Errors.Last().Err, &apiErr)) + require.ErrorAs(t, c.Errors.Last().Err, &apiErr) assert.Equal(t, http.StatusBadRequest, apiErr.Code) assert.Equal(t, "invalid input", apiErr.Msg) assert.True(t, c.IsAborted()) diff --git a/internal/shared/response/response.go b/internal/shared/response/response.go index 14e32b0f..185972b1 100644 --- a/internal/shared/response/response.go +++ b/internal/shared/response/response.go @@ -16,8 +16,8 @@ type Response[T any] struct { // Any 用于 Swagger 文档的响应类型(非泛型) // swag 不支持泛型,使用此类型替代 Response[T] type Any struct { - ErrorMsg string `json:"error_msg" example:""` - Data interface{} `json:"data"` + ErrorMsg string `json:"error_msg" example:""` + Data any `json:"data"` } // APIError 统一的 API 业务错误类型,可被全局错误处理中间件捕获 diff --git a/internal/testhelper/test_helper.go b/internal/testhelper/test_helper.go index e7b3c2c4..988140cf 100644 --- a/internal/testhelper/test_helper.go +++ b/internal/testhelper/test_helper.go @@ -32,6 +32,7 @@ const ( // SetupTestEnvironment initializes an in-memory SQLite DB, seeds default configurations, // starts miniredis, and overrides the global db/Redis clients. It returns a cleanup function. func SetupTestEnvironment(t *testing.T) (*gorm.DB, *miniredis.Miniredis, func()) { + t.Helper() // Initialize GORM in-memory SQLite sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ DisableForeignKeyConstraintWhenMigrating: true, @@ -332,6 +333,7 @@ func getSeedConfigsPart2() []model.SystemConfig { } func seedDefaultConfigs(t *testing.T, tx *gorm.DB) { + t.Helper() defaultConfigs := append(getSeedConfigsPart1(), getSeedConfigsPart2()...) if err := tx.Create(&defaultConfigs).Error; err != nil { diff --git a/pkg/cache/disk/cache.go b/pkg/cache/disk/cache.go index 2dbc0590..33758175 100644 --- a/pkg/cache/disk/cache.go +++ b/pkg/cache/disk/cache.go @@ -123,7 +123,10 @@ func (c *Cache) Set(key string, value []byte, ttl time.Duration) error { // Update memory tracker if elem, ok := c.items[key]; ok { - item := elem.Value.(*cacheItem) + item, ok := elem.Value.(*cacheItem) + if !ok { + return fmt.Errorf("cache: evict list entry for %q has invalid type %T", key, elem.Value) + } c.currentSize += size - item.size item.size = size item.expiredAt = expiredAt @@ -154,7 +157,11 @@ func (c *Cache) Get(key string) ([]byte, error) { return nil, ErrCacheMiss } - item := elem.Value.(*cacheItem) + item, ok := elem.Value.(*cacheItem) + if !ok { + c.mu.RUnlock() + return nil, ErrCacheMiss + } if !item.expiredAt.IsZero() && time.Now().After(item.expiredAt) { c.mu.RUnlock() return c.getAndDeleteIfExpired(key) @@ -204,7 +211,11 @@ func (c *Cache) getAndDeleteIfExpired(key string) ([]byte, error) { return nil, ErrCacheMiss } - item := elem.Value.(*cacheItem) + item, ok := elem.Value.(*cacheItem) + if !ok { + _ = c.deleteUnlocked(key) + return nil, ErrCacheMiss + } if !item.expiredAt.IsZero() && time.Now().After(item.expiredAt) { _ = c.deleteUnlocked(key) return nil, ErrCacheMiss @@ -234,8 +245,9 @@ func (c *Cache) Delete(key string) error { func (c *Cache) deleteUnlocked(key string) error { if elem, ok := c.items[key]; ok { - item := elem.Value.(*cacheItem) - c.currentSize -= item.size + if item, ok := elem.Value.(*cacheItem); ok { + c.currentSize -= item.size + } c.evictList.Remove(elem) delete(c.items, key) } @@ -288,7 +300,11 @@ func (c *Cache) evict() { for c.currentSize > c.maxSize && c.evictList.Len() > 0 { elem := c.evictList.Back() - item := elem.Value.(*cacheItem) + item, ok := elem.Value.(*cacheItem) + if !ok { + c.evictList.Remove(elem) + continue + } c.currentSize -= item.size c.evictList.Remove(elem) delete(c.items, item.key) @@ -380,7 +396,12 @@ func (c *Cache) cleanExpired() { now := time.Now() for key, elem := range c.items { - item := elem.Value.(*cacheItem) + item, ok := elem.Value.(*cacheItem) + if !ok { + c.evictList.Remove(elem) + delete(c.items, key) + continue + } if !item.expiredAt.IsZero() && now.After(item.expiredAt) { c.currentSize -= item.size c.evictList.Remove(elem) diff --git a/pkg/cache/ram/manager.go b/pkg/cache/ram/manager.go index 1198a70a..92245ece 100644 --- a/pkg/cache/ram/manager.go +++ b/pkg/cache/ram/manager.go @@ -6,6 +6,7 @@ package ram import ( "context" "errors" + "maps" "sync" "time" ) @@ -119,9 +120,7 @@ func Set(item CacheItem) { currentMap, ok := managerCache.GetIfPresent(item.Type) newMap := make(map[string]cacheEntry) if ok { - for k, v := range currentMap { - newMap[k] = v - } + maps.Copy(newMap, currentMap) } var expireAt time.Time diff --git a/pkg/cap/cap.go b/pkg/cap/cap.go index 0d3727f0..904dffc2 100644 --- a/pkg/cap/cap.go +++ b/pkg/cap/cap.go @@ -215,7 +215,7 @@ func VerifyChallengeSolutions(token string, solutions []int, secret []byte, expe } tokenFnv := fnv1a(token) - for i := 0; i < payload.Count; i++ { + for i := range payload.Count { idxStr := strconv.Itoa(i + 1) saltSeed := fnv1aResume(tokenFnv, idxStr) targetSeed := fnv1aResume(saltSeed, "d") @@ -238,14 +238,14 @@ func VerifyChallengeSolutions(token string, solutions []int, secret []byte, expe func Solve(token string, count, size, difficulty int) []int { solutions := make([]int, count) tokenFnv := fnv1a(token) - for i := 0; i < count; i++ { + for i := range count { idxStr := strconv.Itoa(i + 1) saltSeed := fnv1aResume(tokenFnv, idxStr) targetSeed := fnv1aResume(saltSeed, "d") salt := prngFromHash(saltSeed, size) target := prngFromHash(targetSeed, difficulty) - for nonce := 0; nonce < 1000000; nonce++ { + for nonce := range 1000000 { hashInput := salt + strconv.Itoa(nonce) hashBytes := sha256.Sum256([]byte(hashInput)) hashHex := hex.EncodeToString(hashBytes[:]) diff --git a/pkg/cap/prng.go b/pkg/cap/prng.go index 46df81dc..0a21dd1e 100644 --- a/pkg/cap/prng.go +++ b/pkg/cap/prng.go @@ -13,7 +13,7 @@ import ( //nolint:mnd // FNV-1a 算法位移常量 func fnv1a(str string) uint32 { var hash uint32 = 2166136261 - for i := 0; i < len(str); i++ { + for i := range len(str) { hash ^= uint32(str[i]) hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24) } @@ -25,7 +25,7 @@ func fnv1a(str string) uint32 { //nolint:mnd // FNV-1a 算法位移常量 func fnv1aResume(state uint32, str string) uint32 { h := state - for i := 0; i < len(str); i++ { + for i := range len(str) { h ^= uint32(str[i]) h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24) } diff --git a/pkg/cap/store.go b/pkg/cap/store.go index 4e7b61f0..05595865 100644 --- a/pkg/cap/store.go +++ b/pkg/cap/store.go @@ -5,6 +5,7 @@ package cap import ( "context" + "errors" "sync" "time" @@ -51,20 +52,21 @@ func NewMemoryStore(cleanupInterval time.Duration) *MemoryStore { func (s *MemoryStore) Get(_ context.Context, key string) (string, bool, error) { s.mu.Lock() defer s.mu.Unlock() - return s.getLocked(key) + val, ok := s.getLocked(key) + return val, ok, nil } // getLocked is the internal helper – caller must hold s.mu. -func (s *MemoryStore) getLocked(key string) (string, bool, error) { +func (s *MemoryStore) getLocked(key string) (string, bool) { item, found := s.items[key] if !found { - return "", false, nil + return "", false } if time.Now().After(item.expiresAt) { delete(s.items, key) - return "", false, nil + return "", false } - return item.value, true, nil + return item.value, true } // Set 向 MemoryStore 写入指定 key 的值 @@ -92,7 +94,7 @@ func (s *MemoryStore) SetNX(_ context.Context, key string, val string, ttl time. s.mu.Lock() defer s.mu.Unlock() - _, exists, _ := s.getLocked(key) + _, exists := s.getLocked(key) if exists { return false, nil } @@ -108,9 +110,9 @@ func (s *MemoryStore) GetAndDelete(_ context.Context, key string) (string, bool, s.mu.Lock() defer s.mu.Unlock() - val, exists, err := s.getLocked(key) - if err != nil || !exists { - return "", false, err + val, exists := s.getLocked(key) + if !exists { + return "", false, nil } delete(s.items, key) return val, true, nil @@ -149,7 +151,7 @@ func NewRedisStore(client redis.UniversalClient) *RedisStore { // Get 从 RedisStore 获取指定 key 的值 func (s *RedisStore) Get(ctx context.Context, key string) (string, bool, error) { val, err := s.client.Get(ctx, key).Result() - if err == redis.Nil { + if errors.Is(err, redis.Nil) { return "", false, nil } if err != nil { @@ -176,7 +178,7 @@ func (s *RedisStore) SetNX(ctx context.Context, key string, val string, ttl time // GetAndDelete wraps Redis GETDEL (available since Redis 6.2). func (s *RedisStore) GetAndDelete(ctx context.Context, key string) (string, bool, error) { val, err := s.client.GetDel(ctx, key).Result() - if err == redis.Nil { + if errors.Is(err, redis.Nil) { return "", false, nil } if err != nil { diff --git a/pkg/geoip/country_centroid.go b/pkg/geoip/country_centroid.go index 3697ff34..09df6d53 100644 --- a/pkg/geoip/country_centroid.go +++ b/pkg/geoip/country_centroid.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package geoip import "strings" @@ -194,7 +197,7 @@ func CountryCentroidByName(name string) (lat float64, lon float64, ok bool) { return v.lat, v.lon, true } // Try comma-separated parts (city / region / country / ISO). - for _, part := range strings.Split(name, ",") { + for part := range strings.SplitSeq(name, ",") { part = strings.TrimSpace(part) if part == "" { continue diff --git a/pkg/geoip/country_centroid_test.go b/pkg/geoip/country_centroid_test.go index d70d316d..c111fd70 100644 --- a/pkg/geoip/country_centroid_test.go +++ b/pkg/geoip/country_centroid_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package geoip import "testing" diff --git a/pkg/geoip/emptyProvider.go b/pkg/geoip/emptyProvider.go index 6d689a0d..3e51fe94 100644 --- a/pkg/geoip/emptyProvider.go +++ b/pkg/geoip/emptyProvider.go @@ -1,7 +1,10 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package geoip import ( - "fmt" + "errors" "net" ) @@ -20,12 +23,12 @@ func (e *EmptyProvider) Initialize() error { // GetGeoInfo reports that no GeoIP provider has been configured. func (e *EmptyProvider) GetGeoInfo(_ net.IP) (*GeoInfo, error) { - return nil, fmt.Errorf("you are using an empty GeoIP provider, please set a valid provider") + return nil, errors.New("you are using an empty GeoIP provider, please set a valid provider") } // UpdateDatabase reports that no GeoIP provider has been configured. func (e *EmptyProvider) UpdateDatabase() error { - return fmt.Errorf("you are using an empty GeoIP provider, please set a valid provider") + return errors.New("you are using an empty GeoIP provider, please set a valid provider") } // Close releases resources held by the empty provider. diff --git a/pkg/geoip/geoip.go b/pkg/geoip/geoip.go index aab42ce6..2c918f7b 100644 --- a/pkg/geoip/geoip.go +++ b/pkg/geoip/geoip.go @@ -1,7 +1,11 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package geoip resolves geographic information for IP addresses. package geoip import ( + "errors" "fmt" "log/slog" "net" @@ -138,7 +142,7 @@ func InitGeoIP(provider string) { // GetGeoInfo looks up geographic information for ip using the active provider. func GetGeoInfo(ip net.IP) (*GeoInfo, error) { if ip == nil { - return nil, fmt.Errorf("IP address cannot be nil") + return nil, errors.New("IP address cannot be nil") } provider := getProvider() cacheKey := provider.Name() + ":" + ip.String() @@ -157,7 +161,7 @@ func GetGeoInfo(ip net.IP) (*GeoInfo, error) { // LookupGeoInfoWithProvider looks up geographic information using a temporary provider. func LookupGeoInfoWithProvider(providerName string, ip net.IP) (*GeoInfo, error) { if ip == nil { - return nil, fmt.Errorf("IP address cannot be nil") + return nil, errors.New("IP address cannot be nil") } provider, err := providerFactory(normalizeProvider(providerName)) diff --git a/pkg/geoip/geoip_test.go b/pkg/geoip/geoip_test.go index d606cfed..334a1401 100644 --- a/pkg/geoip/geoip_test.go +++ b/pkg/geoip/geoip_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package geoip import ( diff --git a/pkg/geoip/geojs.go b/pkg/geoip/geojs.go index 7196f0e0..0f6782fc 100644 --- a/pkg/geoip/geojs.go +++ b/pkg/geoip/geojs.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package geoip import ( diff --git a/pkg/geoip/ipapi.go b/pkg/geoip/ipapi.go index 6378242d..c1c823e7 100644 --- a/pkg/geoip/ipapi.go +++ b/pkg/geoip/ipapi.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package geoip import ( diff --git a/pkg/geoip/ipinfo.go b/pkg/geoip/ipinfo.go index c7a97d50..0a7bdb17 100644 --- a/pkg/geoip/ipinfo.go +++ b/pkg/geoip/ipinfo.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package geoip import ( diff --git a/pkg/geoip/iputil/iputil.go b/pkg/geoip/iputil/iputil.go index 3dc7243c..d6078854 100644 --- a/pkg/geoip/iputil/iputil.go +++ b/pkg/geoip/iputil/iputil.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package iputil provides helpers for parsing, normalizing, and scoring IP addresses. package iputil diff --git a/pkg/geoip/iputil/iputil_test.go b/pkg/geoip/iputil/iputil_test.go index c05a5724..8a642eac 100644 --- a/pkg/geoip/iputil/iputil_test.go +++ b/pkg/geoip/iputil/iputil_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package iputil import ( diff --git a/pkg/geoip/mmdb.go b/pkg/geoip/mmdb.go index 94d263c8..18132f5f 100644 --- a/pkg/geoip/mmdb.go +++ b/pkg/geoip/mmdb.go @@ -1,7 +1,11 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package geoip import ( "context" + "errors" "fmt" "io" "net" @@ -96,10 +100,10 @@ func (s *MaxMindGeoIPService) GetGeoInfo(ip net.IP) (*GeoInfo, error) { defer s.mu.RUnlock() if s.maxMindDBReader == nil { - return nil, fmt.Errorf("MaxMind database is not initialized or failed to open") + return nil, errors.New("MaxMind database is not initialized or failed to open") } if ip == nil { - return nil, fmt.Errorf("IP address cannot be nil") + return nil, errors.New("IP address cannot be nil") } var record Record diff --git a/pkg/geoip/outboundip.go b/pkg/geoip/outboundip.go index 6f7a82ff..50990dba 100644 --- a/pkg/geoip/outboundip.go +++ b/pkg/geoip/outboundip.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package geoip import ( diff --git a/pkg/geoip/outboundip_test.go b/pkg/geoip/outboundip_test.go index 6dd8a232..cfa4b21d 100644 --- a/pkg/geoip/outboundip_test.go +++ b/pkg/geoip/outboundip_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package geoip import ( diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 7f9ba192..ba9bedb3 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -78,25 +78,25 @@ func Init(cfg Config) { } // DebugF 输出 Debug 级别日志 -func DebugF(ctx context.Context, format string, args ...interface{}) { +func DebugF(ctx context.Context, format string, args ...any) { msg := fmt.Sprintf(format, args...) logger.Ctx(ctx).Debug(msg, getTraceIDFields(ctx)...) } // InfoF 输出 Info 级别日志 -func InfoF(ctx context.Context, format string, args ...interface{}) { +func InfoF(ctx context.Context, format string, args ...any) { msg := fmt.Sprintf(format, args...) logger.Ctx(ctx).Info(msg, getTraceIDFields(ctx)...) } // WarnF 输出 Warn 级别日志 -func WarnF(ctx context.Context, format string, args ...interface{}) { +func WarnF(ctx context.Context, format string, args ...any) { msg := fmt.Sprintf(format, args...) logger.Ctx(ctx).Warn(msg, getTraceIDFields(ctx)...) } // ErrorF 输出 Error 级别日志 -func ErrorF(ctx context.Context, format string, args ...interface{}) { +func ErrorF(ctx context.Context, format string, args ...any) { msg := fmt.Sprintf(format, args...) logger.Ctx(ctx).Error(msg, getTraceIDFields(ctx)...) } diff --git a/pkg/logger/ringbuffer.go b/pkg/logger/ringbuffer.go index 71a77f37..89fe211c 100644 --- a/pkg/logger/ringbuffer.go +++ b/pkg/logger/ringbuffer.go @@ -47,7 +47,7 @@ func (r *LogRingBuffer) Write(p []byte) (int, error) { data := string(p) start := 0 - for i := 0; i < len(data); i++ { + for i := range len(data) { if data[i] == '\n' { line := data[start:i] start = i + 1 @@ -114,7 +114,7 @@ func (r *LogRingBuffer) Query(cursor int, limit int) ([]LogEntry, bool) { // 将 ring buffer 中的有效条目按顺序收集 ordered := make([]LogEntry, 0, r.count) - for i := 0; i < r.count; i++ { + for i := range r.count { pos := (oldestPos + i) % r.cap ordered = append(ordered, r.entries[pos]) } @@ -141,10 +141,7 @@ func (r *LogRingBuffer) Query(cursor int, limit int) ([]LogEntry, bool) { } // 返回 cut 之前的最后 limit 条 - start := cut - limit - if start < 0 { - start = 0 - } + start := max(cut-limit, 0) hasMore := start > 0 return ordered[start:cut], hasMore diff --git a/pkg/logger/ringbuffer_test.go b/pkg/logger/ringbuffer_test.go index f3c7e787..249a74ed 100644 --- a/pkg/logger/ringbuffer_test.go +++ b/pkg/logger/ringbuffer_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestLogRingBuffer_WriteAndQuery(t *testing.T) { @@ -18,7 +19,7 @@ func TestLogRingBuffer_WriteAndQuery(t *testing.T) { entries, hasMore := rb.Query(0, 10) assert.False(t, hasMore) - assert.Equal(t, 3, len(entries)) + assert.Len(t, entries, 3) assert.Equal(t, "line1", entries[0].Data) assert.Equal(t, "line2", entries[1].Data) assert.Equal(t, "line3", entries[2].Data) @@ -34,7 +35,7 @@ func TestLogRingBuffer_CapacityOverflow(t *testing.T) { entries, hasMore := rb.Query(0, 10) assert.False(t, hasMore) - assert.Equal(t, 3, len(entries)) + assert.Len(t, entries, 3) assert.Equal(t, "c", entries[0].Data) assert.Equal(t, "d", entries[1].Data) assert.Equal(t, "e", entries[2].Data) @@ -48,7 +49,7 @@ func TestLogRingBuffer_QueryLatest(t *testing.T) { // Query latest 2 entries, hasMore := rb.Query(0, 2) assert.True(t, hasMore) - assert.Equal(t, 2, len(entries)) + assert.Len(t, entries, 2) assert.Equal(t, "d", entries[0].Data) assert.Equal(t, "e", entries[1].Data) } @@ -60,12 +61,12 @@ func TestLogRingBuffer_QueryByCursor(t *testing.T) { // First get all to find indices all, _ := rb.Query(0, 10) - assert.Equal(t, 5, len(all)) + assert.Len(t, all, 5) // Query entries before index 3 entries, hasMore := rb.Query(3, 10) assert.False(t, hasMore) - assert.Equal(t, 3, len(entries)) + assert.Len(t, entries, 3) assert.Equal(t, "a", entries[0].Data) assert.Equal(t, "b", entries[1].Data) assert.Equal(t, "c", entries[2].Data) @@ -79,7 +80,7 @@ func TestLogRingBuffer_QueryByCursorWithLimit(t *testing.T) { // Query 2 entries before index 4 entries, hasMore := rb.Query(4, 2) assert.True(t, hasMore) - assert.Equal(t, 2, len(entries)) + assert.Len(t, entries, 2) assert.Equal(t, "c", entries[0].Data) assert.Equal(t, "d", entries[1].Data) } @@ -98,7 +99,7 @@ func TestLogRingBuffer_QueryNonExistentCursor(t *testing.T) { entries, hasMore := rb.Query(999, 10) assert.False(t, hasMore) - assert.Equal(t, 2, len(entries)) + assert.Len(t, entries, 2) assert.Equal(t, "a", entries[0].Data) assert.Equal(t, "b", entries[1].Data) } @@ -138,7 +139,7 @@ func TestLogRingBuffer_WriteNoNewline(t *testing.T) { _, _ = rb.Write([]byte("partial")) entries, _ := rb.Query(0, 10) - assert.Equal(t, 1, len(entries)) + assert.Len(t, entries, 1) assert.Equal(t, "partial", entries[0].Data) } @@ -147,7 +148,7 @@ func TestLogRingBuffer_WriteEmpty(t *testing.T) { n, err := rb.Write([]byte("")) assert.Equal(t, 0, n) - assert.NoError(t, err) + require.NoError(t, err) entries, _ := rb.Query(0, 10) assert.Nil(t, entries) @@ -160,7 +161,7 @@ func TestLogRingBuffer_QueryAfterOverflow(t *testing.T) { entries, hasMore := rb.Query(0, 10) assert.False(t, hasMore) - assert.Equal(t, 3, len(entries)) + assert.Len(t, entries, 3) assert.Equal(t, "5", entries[0].Data) assert.Equal(t, "6", entries[1].Data) assert.Equal(t, "7", entries[2].Data) @@ -178,14 +179,14 @@ func TestLogRingBuffer_NextCursor(t *testing.T) { // Query latest 2, should return next_cursor pointing to first returned entry entries, _ := rb.Query(0, 2) - assert.Equal(t, 2, len(entries)) + assert.Len(t, entries, 2) // entries[0].Index = 3 ("d"), entries[1].Index = 4 ("e") assert.Equal(t, 3, entries[0].Index) // Now use that index as cursor to get older entries older, hasMore := rb.Query(entries[0].Index, 10) assert.False(t, hasMore) - assert.Equal(t, 3, len(older)) + assert.Len(t, older, 3) assert.Equal(t, "a", older[0].Data) assert.Equal(t, "b", older[1].Data) assert.Equal(t, "c", older[2].Data) diff --git a/pkg/mail/mail.go b/pkg/mail/mail.go index ed34ccad..1321d1cf 100644 --- a/pkg/mail/mail.go +++ b/pkg/mail/mail.go @@ -55,21 +55,21 @@ func SendMailHTML(ctx context.Context, cfg Config, to string, subject, body stri header["MIME-Version"] = "1.0" header["Content-Type"] = "text/html; charset=UTF-8" - message := "" + var message strings.Builder for k, v := range header { - message += fmt.Sprintf("%s: %s\r\n", k, v) + fmt.Fprintf(&message, "%s: %s\r\n", k, v) } - message += "\r\n" + body + message.WriteString("\r\n" + body) auth := smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host) // If using SSL port 465, we connection via TLS dial if cfg.Port == smtpSSLPort { - return sendMailViaSSL(ctx, addr, auth, cfg, to, message) + return sendMailViaSSL(ctx, addr, auth, cfg, to, message.String()) } // For standard port (587 / 25), use smtp.SendMail directly (handles STARTTLS automatically if server supports it) - err := smtp.SendMail(addr, auth, cfg.Username, []string{to}, []byte(message)) + err := smtp.SendMail(addr, auth, cfg.Username, []string{to}, []byte(message.String())) if err != nil { return fmt.Errorf(errSendMailFailed, err) } @@ -127,7 +127,7 @@ func sendMailViaSSL(ctx context.Context, addr string, auth smtp.Auth, cfg Config // SendMailWithLog sends a test email and records a detailed SMTP connection log func SendMailWithLog(ctx context.Context, cfg Config, to string, subject, body string) (string, error) { var logBuf bytes.Buffer - logLine := func(dir string, format string, args ...interface{}) { + logLine := func(dir string, format string, args ...any) { fmt.Fprintf(&logBuf, "[%s] %s\n", dir, fmt.Sprintf(format, args...)) } @@ -227,14 +227,14 @@ func SendMailWithLog(ctx context.Context, cfg Config, to string, subject, body s header["MIME-Version"] = "1.0" header["Content-Type"] = "text/html; charset=UTF-8" - message := "" + var message strings.Builder for k, v := range header { - message += fmt.Sprintf("%s: %s\r\n", k, v) + fmt.Fprintf(&message, "%s: %s\r\n", k, v) } - message += "\r\n" + body + message.WriteString("\r\n" + body) logLine("System", "Sending message body...") - if _, err = w.Write([]byte(message)); err != nil { + if _, err = w.Write([]byte(message.String())); err != nil { _ = w.Close() logLine("Error", "Writing message body failed: %v", err) return logBuf.String(), err diff --git a/pkg/pagesarchive/entry.go b/pkg/pagesarchive/entry.go index ce19734b..a236a407 100644 --- a/pkg/pagesarchive/entry.go +++ b/pkg/pagesarchive/entry.go @@ -72,14 +72,14 @@ func copyLimited(dst io.Writer, src io.Reader, maxBytes int64) (int64, error) { return written, err } if written > maxBytes { - return written, fmt.Errorf("pages file size out of bounds") + return written, errors.New("pages file size out of bounds") } return written, nil } func copyAndVerifySize(dst io.Writer, src io.Reader, declaredSize uint64, maxBytes int64) (int64, error) { if declaredSize > uint64(math.MaxInt64) { - return 0, fmt.Errorf("pages file size out of bounds") + return 0, errors.New("pages file size out of bounds") } written, err := copyLimited(dst, src, maxBytes) if err != nil { diff --git a/pkg/pagesarchive/extract.go b/pkg/pagesarchive/extract.go index c8eb1426..2da82c41 100644 --- a/pkg/pagesarchive/extract.go +++ b/pkg/pagesarchive/extract.go @@ -124,7 +124,7 @@ func extractEntries(entries []Entry, destDir string, opts ExtractOptions) error appendMeasuredFile(measured, normalizedPath, actual) } if measured.fileCount == 0 { - return fmt.Errorf("pages package is empty") + return errors.New("pages package is empty") } return nil } @@ -139,7 +139,7 @@ func extractTarFamilyAt(ra io.ReaderAt, size int64, format Format, destDir strin return err } if firstPass.fileCount == 0 { - return fmt.Errorf("pages package is empty") + return errors.New("pages package is empty") } commonPrefix := "" if opts.StripCommonRoot { @@ -162,7 +162,7 @@ func extractTarFamilyAt(ra io.ReaderAt, size int64, format Format, destDir strin return extractErr } if secondPass.fileCount != firstPass.fileCount || secondPass.totalSize != firstPass.totalSize { - return fmt.Errorf("pages tar package changed between validation and extraction") + return errors.New("pages tar package changed between validation and extraction") } return nil } @@ -177,7 +177,7 @@ func extractTarReader( measured := &measuredArchive{files: make([]measuredFile, 0)} for { header, err := tarReader.Next() - if err == io.EOF { + if errors.Is(err, io.EOF) { break } if err != nil { diff --git a/pkg/pagesarchive/format.go b/pkg/pagesarchive/format.go index 99fde833..279247ab 100644 --- a/pkg/pagesarchive/format.go +++ b/pkg/pagesarchive/format.go @@ -7,7 +7,7 @@ package pagesarchive import ( "bytes" - "fmt" + "errors" "path/filepath" "strings" ) @@ -87,7 +87,7 @@ func DetectFormat(fileName string, data []byte) (Format, error) { if format, ok := DetectFormatFromBytes(data); ok { return format, nil } - return "", fmt.Errorf("unsupported pages package format") + return "", errors.New("unsupported pages package format") } // Extension returns the canonical file extension for a format (without leading dot). diff --git a/pkg/pagesarchive/inspect.go b/pkg/pagesarchive/inspect.go index ae3a3125..15b27b8f 100644 --- a/pkg/pagesarchive/inspect.go +++ b/pkg/pagesarchive/inspect.go @@ -137,7 +137,7 @@ func scanTarFamilyAt( enforceLimits bool, ) (*measuredArchive, error) { if size < 0 { - return nil, fmt.Errorf("invalid pages package size") + return nil, errors.New("invalid pages package size") } tarReader, closeReader, err := openTarFamilyReader(io.NewSectionReader(ra, 0, size), format) if err != nil { @@ -154,7 +154,7 @@ func scanTarReader(tarReader *tar.Reader, limits Limits, enforceLimits bool) (*m measured := &measuredArchive{files: make([]measuredFile, 0)} for { header, err := tarReader.Next() - if err == io.EOF { + if errors.Is(err, io.EOF) { break } if err != nil { @@ -183,7 +183,7 @@ func scanTarReader(tarReader *tar.Reader, limits Limits, enforceLimits bool) (*m func buildMeasuredManifest(measured *measuredArchive, opts InspectOptions) (*Manifest, error) { if measured == nil || measured.fileCount == 0 { - return nil, fmt.Errorf("pages package is empty") + return nil, errors.New("pages package is empty") } targetEntryPath, err := resolveTargetEntryPath(opts.RootDir, opts.EntryFile) if err != nil { @@ -231,7 +231,7 @@ func prepareMeasuredFile(measured *measuredArchive, normalizedPath string, decla } remaining := limits.MaxTotalBytes - measured.totalSize if remaining < 0 || declaredSize > uint64(remaining) { //nolint:gosec // remaining is checked non-negative - return fmt.Errorf("pages extracted size exceeds limit") + return errors.New("pages extracted size exceeds limit") } return nil } @@ -295,6 +295,8 @@ func isTarFamily(format Format) bool { switch format { case FormatTar, FormatTarGz, FormatTarXz, FormatTarBz2: return true + case FormatZip, FormatSevenZip: + return false default: return false } diff --git a/pkg/pagesarchive/list.go b/pkg/pagesarchive/list.go index e5781f08..003573be 100644 --- a/pkg/pagesarchive/list.go +++ b/pkg/pagesarchive/list.go @@ -8,6 +8,7 @@ import ( "archive/zip" "compress/bzip2" "compress/gzip" + "errors" "fmt" "io" "os" @@ -52,13 +53,15 @@ func (z sevenZipArchiveFile) Open() (io.ReadCloser, error) { // Tar-family archives use the sequential streaming paths in inspect.go/extract.go. func listRandomAccessEntriesAt(ra io.ReaderAt, size int64, format Format) ([]Entry, error) { if size < 0 { - return nil, fmt.Errorf("invalid pages package size") + return nil, errors.New("invalid pages package size") } switch format { case FormatZip: return listZipEntriesAt(ra, size) case FormatSevenZip: return listSevenZipEntriesAt(ra, size) + case FormatTar, FormatTarGz, FormatTarXz, FormatTarBz2: + return nil, fmt.Errorf("unsupported random-access pages package format: %s", format) default: return nil, fmt.Errorf("unsupported random-access pages package format: %s", format) } @@ -126,6 +129,8 @@ func openTarFamilyReader(r io.Reader, format Format) (*tar.Reader, func() error, return tar.NewReader(xzReader), func() error { return nil }, nil case FormatTarBz2: return tar.NewReader(bzip2.NewReader(r)), func() error { return nil }, nil + case FormatZip, FormatSevenZip: + return nil, nil, fmt.Errorf("unsupported tar family format: %s", format) default: return nil, nil, fmt.Errorf("unsupported tar family format: %s", format) } diff --git a/pkg/pagesarchive/path.go b/pkg/pagesarchive/path.go index 94562c8b..8f7d2b38 100644 --- a/pkg/pagesarchive/path.go +++ b/pkg/pagesarchive/path.go @@ -4,6 +4,7 @@ package pagesarchive import ( + "errors" "fmt" "path" "strings" @@ -18,7 +19,7 @@ func NormalizeLogicalPath(raw string, allowEmpty bool) (string, error) { if allowEmpty { return "", nil } - return "", fmt.Errorf("pages path is required") + return "", errors.New("pages path is required") } if err := validateLogicalPathText(raw); err != nil { return "", err @@ -29,7 +30,7 @@ func NormalizeLogicalPath(raw string, allowEmpty bool) (string, error) { if allowEmpty { return "", nil } - return "", fmt.Errorf("pages path is required") + return "", errors.New("pages path is required") } if strings.HasPrefix(cleaned, "/") || cleaned == ".." || strings.HasPrefix(cleaned, "../") { return "", fmt.Errorf("pages path escapes directory: %s", raw) @@ -39,7 +40,7 @@ func NormalizeLogicalPath(raw string, allowEmpty bool) (string, error) { func validateLogicalPathText(raw string) error { if !utf8.ValidString(raw) { - return fmt.Errorf("pages path is not valid UTF-8") + return errors.New("pages path is not valid UTF-8") } if strings.Contains(raw, "\\") { return fmt.Errorf("pages path must use POSIX separators: %s", raw) @@ -56,7 +57,7 @@ func validateLogicalPathText(raw string) error { func validateLogicalPathRunes(raw string) error { for _, r := range raw { if r == 0 || unicode.IsControl(r) { - return fmt.Errorf("pages path contains a control character") + return errors.New("pages path contains a control character") } if r == '\'' || r == '"' || r == ';' { return fmt.Errorf("pages path contains an unsupported character: %s", raw) @@ -66,7 +67,7 @@ func validateLogicalPathRunes(raw string) error { } func validateLogicalPathSegments(raw string) error { - for _, segment := range strings.Split(raw, "/") { + for segment := range strings.SplitSeq(raw, "/") { if len(segment) >= 2 && segment[1] == ':' { return fmt.Errorf("pages path contains a Windows drive: %s", raw) } diff --git a/pkg/protocol/server.go b/pkg/protocol/server.go index cd473b6f..bff3e1d6 100644 --- a/pkg/protocol/server.go +++ b/pkg/protocol/server.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package protocol // AgentNodeSystemProfile is an alias for NodeSystemProfile used by server. diff --git a/pkg/push/custom_test.go b/pkg/push/custom_test.go index fb622e65..0d3e61bc 100644 --- a/pkg/push/custom_test.go +++ b/pkg/push/custom_test.go @@ -82,7 +82,7 @@ func TestCustomPusherSend_ResponseBodyErrcode(t *testing.T) { assert.Contains(t, err.Error(), tt.wantErrMsg) return } - assert.NoError(t, err) + require.NoError(t, err) if tt.body != "" { assert.Contains(t, upstreamResp, tt.body) } diff --git a/pkg/push/email.go b/pkg/push/email.go index 50635aa0..98ff5f64 100644 --- a/pkg/push/email.go +++ b/pkg/push/email.go @@ -41,7 +41,7 @@ func (p *EmailPusher) Send(ctx context.Context, cfg Config, target string, body title = t } - content := "" + var content string if c, ok := body["content"].(string); ok && c != "" { content = c } else { diff --git a/pkg/push/lark.go b/pkg/push/lark.go index e97a8b1a..a33b310f 100644 --- a/pkg/push/lark.go +++ b/pkg/push/lark.go @@ -160,7 +160,7 @@ func (p *LarkPusher) Send(ctx context.Context, cfg Config, _ string, body map[st title = t } - content := "" + var content string if c, ok := body["content"].(string); ok && c != "" { content = c } else { @@ -265,7 +265,7 @@ func (p *LarkPusher) ValidateConfig(cfg Config) error { } func larkSign(secret string, timestamp int64) (string, error) { - stringToSign := fmt.Sprintf("%v", timestamp) + "\n" + secret + stringToSign := strconv.FormatInt(timestamp, 10) + "\n" + secret h := hmac.New(sha256.New, []byte(stringToSign)) _, err := h.Write(nil) if err != nil { diff --git a/pkg/push/telegram.go b/pkg/push/telegram.go index 54f49e76..72a95070 100644 --- a/pkg/push/telegram.go +++ b/pkg/push/telegram.go @@ -60,7 +60,7 @@ func (p *TelegramPusher) Send(ctx context.Context, cfg Config, target string, bo if t, ok := body["title"].(string); ok && t != "" { title = t } - content := "" + var content string if c, ok := body["content"].(string); ok && c != "" { content = c } else { diff --git a/pkg/push/telegram_test.go b/pkg/push/telegram_test.go index 74fcf728..6552b61d 100644 --- a/pkg/push/telegram_test.go +++ b/pkg/push/telegram_test.go @@ -23,7 +23,7 @@ func TestTelegramPusher_Send(t *testing.T) { assert.Equal(t, "application/json", r.Header.Get("Content-Type")) err := json.NewDecoder(r.Body).Decode(&receivedReq) - require.NoError(t, err) + assert.NoError(t, err) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"ok": true}`)) @@ -55,7 +55,7 @@ func TestTelegramPusher_Send(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req telegramMessageRequest err := json.NewDecoder(r.Body).Decode(&req) - require.NoError(t, err) + assert.NoError(t, err) requests = append(requests, &req) if len(requests) == 1 { @@ -84,7 +84,7 @@ func TestTelegramPusher_Send(t *testing.T) { require.Len(t, requests, 2) assert.Equal(t, "HTML", requests[0].ParseMode) - assert.Equal(t, "", requests[1].ParseMode) + assert.Empty(t, requests[1].ParseMode) assert.Contains(t, requests[1].Text, "[INFO] Alert & Info") assert.Contains(t, requests[1].Text, "A < B comparison") }) @@ -96,7 +96,7 @@ func TestTelegramPusher_Send(t *testing.T) { URL: "https://api.telegram.org", } err := pusher.ValidateConfig(cfg) - assert.Error(t, err) + require.Error(t, err) cfg = Config{ Channel: "telegram", @@ -104,7 +104,7 @@ func TestTelegramPusher_Send(t *testing.T) { Secret: "token", } err = pusher.ValidateConfig(cfg) - assert.Error(t, err) + require.Error(t, err) cfg = Config{ Channel: "telegram", diff --git a/pkg/render/openresty/origin_error_page.go b/pkg/render/openresty/origin_error_page.go index 29b18ae5..63608ae9 100644 --- a/pkg/render/openresty/origin_error_page.go +++ b/pkg/render/openresty/origin_error_page.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package openresty import ( diff --git a/pkg/render/openresty/origin_error_page_test.go b/pkg/render/openresty/origin_error_page_test.go index 466c91d6..7bd1450c 100644 --- a/pkg/render/openresty/origin_error_page_test.go +++ b/pkg/render/openresty/origin_error_page_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package openresty import ( diff --git a/pkg/render/openresty/render.go b/pkg/render/openresty/render.go index cdf4d7ba..0b775fb5 100644 --- a/pkg/render/openresty/render.go +++ b/pkg/render/openresty/render.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package openresty renders OpenResty configuration from proxy route definitions. package openresty @@ -14,6 +17,7 @@ import ( "path" "regexp" "sort" + "strconv" "strings" ) @@ -103,7 +107,7 @@ func RenderRouteConfig(doc Document, certificateFiles []SupportFile) (string, er displayName := resolveRouteSiteName(route) cacheConfig := routeCacheConfig{Enabled: route.CacheEnabled, Policy: route.CachePolicy, Rules: route.CacheRules} limitConfig := mergeRouteLimitConfig(route, doc.OpenRestyConfig) - powEnabled, _ := getPoWConfigForRoute(route.ID, doc.WAF) + powEnabled := getPoWConfigForRoute(route.ID, doc.WAF) if normalizeRouteUpstreamType(route.UpstreamType) == routeUpstreamTypePages { if err := renderPagesRoute(&builder, route, displayName, serverNames, certificates, limitConfig, powEnabled, doc.OpenRestyConfig); err != nil { return "", err @@ -168,32 +172,32 @@ func DedupeSupportFiles(files []SupportFile) []SupportFile { func renderMainConfigTemplate(templateText string, cfg ConfigSnapshot, limitReqRates []string) string { replacer := strings.NewReplacer( "{{OpenRestyWorkerProcesses}}", cfg.WorkerProcesses, - "{{OpenRestyWorkerConnections}}", fmt.Sprintf("%d", cfg.WorkerConnections), - "{{OpenRestyWorkerRlimitNofile}}", fmt.Sprintf("%d", cfg.WorkerRlimitNofile), + "{{OpenRestyWorkerConnections}}", strconv.Itoa(cfg.WorkerConnections), + "{{OpenRestyWorkerRlimitNofile}}", strconv.Itoa(cfg.WorkerRlimitNofile), "{{OpenRestyConnectionUpgradeMap}}", renderConnectionUpgradeMap(), "{{OpenRestyDefaultServerBlock}}", renderDefaultServerBlock(cfg.DefaultServerReturnStatus, cfg.HTTP3Enabled), "{{OpenRestyAccessLogPath}}", AccessLogPlaceholder, "{{OpenRestyErrorLogPath}}", ErrorLogPlaceholder, "{{OpenRestyEventsUseDirective}}", renderTemplateDirective(cfg.EventsUse != "", fmt.Sprintf("use %s;", cfg.EventsUse)), "{{OpenRestyEventsMultiAcceptDirective}}", renderTemplateDirective(cfg.EventsMultiAcceptEnabled, "multi_accept on;"), - "{{OpenRestyKeepaliveTimeout}}", fmt.Sprintf("%d", cfg.KeepaliveTimeout), - "{{OpenRestyKeepaliveRequests}}", fmt.Sprintf("%d", cfg.KeepaliveRequests), - "{{OpenRestyClientHeaderTimeout}}", fmt.Sprintf("%d", cfg.ClientHeaderTimeout), - "{{OpenRestyClientBodyTimeout}}", fmt.Sprintf("%d", cfg.ClientBodyTimeout), + "{{OpenRestyKeepaliveTimeout}}", strconv.Itoa(cfg.KeepaliveTimeout), + "{{OpenRestyKeepaliveRequests}}", strconv.Itoa(cfg.KeepaliveRequests), + "{{OpenRestyClientHeaderTimeout}}", strconv.Itoa(cfg.ClientHeaderTimeout), + "{{OpenRestyClientBodyTimeout}}", strconv.Itoa(cfg.ClientBodyTimeout), "{{OpenRestyClientMaxBodySize}}", cfg.ClientMaxBodySize, "{{OpenRestyLargeClientHeaderBuffers}}", cfg.LargeClientHeaderBuffers, - "{{OpenRestySendTimeout}}", fmt.Sprintf("%d", cfg.SendTimeout), - "{{OpenRestyProxyConnectTimeout}}", fmt.Sprintf("%d", cfg.ProxyConnectTimeout), - "{{OpenRestyProxySendTimeout}}", fmt.Sprintf("%d", cfg.ProxySendTimeout), - "{{OpenRestyProxyReadTimeout}}", fmt.Sprintf("%d", cfg.ProxyReadTimeout), + "{{OpenRestySendTimeout}}", strconv.Itoa(cfg.SendTimeout), + "{{OpenRestyProxyConnectTimeout}}", strconv.Itoa(cfg.ProxyConnectTimeout), + "{{OpenRestyProxySendTimeout}}", strconv.Itoa(cfg.ProxySendTimeout), + "{{OpenRestyProxyReadTimeout}}", strconv.Itoa(cfg.ProxyReadTimeout), "{{OpenRestyProxyRequestBuffering}}", onOff(cfg.ProxyRequestBuffering), "{{OpenRestyProxyBuffering}}", onOff(cfg.ProxyBufferingEnabled), "{{OpenRestyProxyBuffers}}", cfg.ProxyBuffers, "{{OpenRestyProxyBufferSize}}", cfg.ProxyBufferSize, "{{OpenRestyProxyBusyBuffersSize}}", cfg.ProxyBusyBuffersSize, "{{OpenRestyGzip}}", onOff(cfg.GzipEnabled), - "{{OpenRestyGzipMinLength}}", fmt.Sprintf("%d", cfg.GzipMinLength), - "{{OpenRestyGzipCompLevel}}", fmt.Sprintf("%d", cfg.GzipCompLevel), + "{{OpenRestyGzipMinLength}}", strconv.Itoa(cfg.GzipMinLength), + "{{OpenRestyGzipCompLevel}}", strconv.Itoa(cfg.GzipCompLevel), "{{OpenRestyResolverDirective}}", renderTemplateDirective(cfg.Resolvers != "", fmt.Sprintf("resolver %s;", cfg.Resolvers)), "{{OpenRestyCacheBlock}}", renderOpenRestyCacheTemplateBlock(cfg, limitReqRates), "{{OpenRestyRouteConfigInclude}}", RouteConfigPlaceholder, @@ -414,7 +418,7 @@ func pagesFallbackPath(deployment *PagesDeployment) string { if value == "/" || strings.HasSuffix(value, "/") || strings.Contains(value, "\\") || strings.ContainsAny(value, "\"';") || strings.ContainsAny(value, " \t\r\n") { return indexHTML } - for _, segment := range strings.Split(value, "/") { + for segment := range strings.SplitSeq(value, "/") { if segment == "." || segment == ".." { return indexHTML } @@ -765,7 +769,7 @@ func renderDefaultServerBlock(statusCode int, http3Enabled bool) string { " }", "", " server {", - fmt.Sprintf(" listen 443 ssl default_server;%s", h3Default), + " listen 443 ssl default_server;" + h3Default, " server_name _;", "", " ssl_reject_handshake on;", @@ -826,7 +830,7 @@ func validateCertificateCoverage(certPEM string, domains []string) error { return nil } -func getPoWConfigForRoute(routeID uint, snapshot WAFDocument) (bool, *PoWConfig) { +func getPoWConfigForRoute(routeID uint, snapshot WAFDocument) bool { enabledGroups := make(map[uint]WAFRuleGroup, len(snapshot.RuleGroups)) globalGroupIDs := make([]uint, 0) for _, group := range snapshot.RuleGroups { @@ -856,10 +860,10 @@ func getPoWConfigForRoute(routeID uint, snapshot WAFDocument) (bool, *PoWConfig) for _, groupID := range activeGroupIDs { group := enabledGroups[groupID] if graphContainsNodeType(group.Graph, "pow") { - return true, nil + return true } } - return false, nil + return false } func graphContainsNodeType(graph WAFRuleGraph, nodeType string) bool { @@ -946,7 +950,7 @@ func buildPathPrefixMatchPattern(rules []string) string { parts = append(parts, "/") continue } - parts = append(parts, fmt.Sprintf("%s(?:/|$)", regexp.QuoteMeta(trimmed))) + parts = append(parts, regexp.QuoteMeta(trimmed)+"(?:/|$)") } return fmt.Sprintf("^(?:%s)", strings.Join(parts, "|")) } diff --git a/pkg/render/openresty/render_route.go b/pkg/render/openresty/render_route.go index 7b0e07f7..e05c9e8e 100644 --- a/pkg/render/openresty/render_route.go +++ b/pkg/render/openresty/render_route.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package openresty import ( diff --git a/pkg/render/openresty/render_test.go b/pkg/render/openresty/render_test.go index cc69918a..ff4a398b 100644 --- a/pkg/render/openresty/render_test.go +++ b/pkg/render/openresty/render_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package openresty import ( @@ -108,13 +111,10 @@ func TestGetPoWConfigForRouteUsesGlobalGroupWithoutExplicitBinding(t *testing.T) }, } - enabled, config := getPoWConfigForRoute(42, snapshot) + enabled := getPoWConfigForRoute(42, snapshot) if !enabled { t.Fatal("expected pow to be enabled via global rule group") } - if config != nil { - t.Fatalf("expected node config to stay in runtime graph, got legacy config %#v", config) - } } func TestRenderRouteConfigEnablesPoWLocationsFromRuntimeGraph(t *testing.T) { diff --git a/pkg/render/openresty/service_worker.go b/pkg/render/openresty/service_worker.go index 44bcd97d..2b3cfb8f 100644 --- a/pkg/render/openresty/service_worker.go +++ b/pkg/render/openresty/service_worker.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package openresty import ( diff --git a/pkg/render/openresty/service_worker_test.go b/pkg/render/openresty/service_worker_test.go index aa02006d..7da60f18 100644 --- a/pkg/render/openresty/service_worker_test.go +++ b/pkg/render/openresty/service_worker_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package openresty import ( diff --git a/pkg/render/openresty/status_codes.go b/pkg/render/openresty/status_codes.go index e7758b0b..f97381aa 100644 --- a/pkg/render/openresty/status_codes.go +++ b/pkg/render/openresty/status_codes.go @@ -4,6 +4,7 @@ package openresty import ( + "errors" "fmt" "sort" "strconv" @@ -22,14 +23,14 @@ const ( func ParseStatusCodeTag(tag string) (lo, hi int, err error) { tag = strings.TrimSpace(tag) if tag == "" { - return 0, 0, fmt.Errorf("状态码标签不能为空") + return 0, 0, errors.New("状态码标签不能为空") } - if i := strings.IndexByte(tag, '-'); i >= 0 { - lo, err = strconv.Atoi(tag[:i]) + if before, after, ok := strings.Cut(tag, "-"); ok { + lo, err = strconv.Atoi(before) if err != nil { return 0, 0, fmt.Errorf("无效状态码区间: %s", tag) } - hi, err = strconv.Atoi(tag[i+1:]) + hi, err = strconv.Atoi(after) if err != nil { return 0, 0, fmt.Errorf("无效状态码区间: %s", tag) } diff --git a/pkg/render/openresty/status_codes_test.go b/pkg/render/openresty/status_codes_test.go index 461672cd..98c82daa 100644 --- a/pkg/render/openresty/status_codes_test.go +++ b/pkg/render/openresty/status_codes_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package openresty import "testing" diff --git a/pkg/render/openresty/types.go b/pkg/render/openresty/types.go index 27f5b91d..4def7a9b 100644 --- a/pkg/render/openresty/types.go +++ b/pkg/render/openresty/types.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package openresty import ( diff --git a/pkg/util/format.go b/pkg/util/format.go index 2aa9f5a9..5b18daba 100644 --- a/pkg/util/format.go +++ b/pkg/util/format.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package util provides shared formatting and string helper functions. package util @@ -22,20 +25,20 @@ const ( // Bytes2Size converts a byte count to a human-readable string with unit (B, KB, MB, GB). func Bytes2Size(num int64) string { - numStr := "" + var numStr string unit := "B" switch { case num/int64(sizeGB) >= 1: numStr = fmt.Sprintf("%.2f", float64(num)/float64(sizeGB)) unit = "GB" case num/int64(sizeMB) >= 1: - numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeMB))) + numStr = strconv.Itoa(int(float64(num) / float64(sizeMB))) unit = "MB" case num/int64(sizeKB) >= 1: - numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeKB))) + numStr = strconv.Itoa(int(float64(num) / float64(sizeKB))) unit = "KB" default: - numStr = fmt.Sprintf("%d", num) + numStr = strconv.FormatInt(num, 10) } return numStr + " " + unit } diff --git a/pkg/util/format_test.go b/pkg/util/format_test.go index 9c200bad..b2fd973f 100644 --- a/pkg/util/format_test.go +++ b/pkg/util/format_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package util import ( diff --git a/pkg/util/network.go b/pkg/util/network.go index 68083a9f..d7b864d5 100644 --- a/pkg/util/network.go +++ b/pkg/util/network.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package util import ( diff --git a/pkg/util/network_test.go b/pkg/util/network_test.go index 94c2493f..62a9bd21 100644 --- a/pkg/util/network_test.go +++ b/pkg/util/network_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package util import ( diff --git a/pkg/util/slice.go b/pkg/util/slice.go index 53025013..74eb7139 100644 --- a/pkg/util/slice.go +++ b/pkg/util/slice.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package util import ( diff --git a/pkg/util/string.go b/pkg/util/string.go index d052a239..9ea84e8d 100644 --- a/pkg/util/string.go +++ b/pkg/util/string.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package util import "strings" diff --git a/pkg/util/value.go b/pkg/util/value.go index 759a62f2..6473bd53 100644 --- a/pkg/util/value.go +++ b/pkg/util/value.go @@ -1,14 +1,20 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package util -import "fmt" +import ( + "fmt" + "strconv" +) // Interface2String converts a string, int, or float64 value to its string representation. -func Interface2String(inter interface{}) string { +func Interface2String(inter any) string { switch v := inter.(type) { case string: return v case int: - return fmt.Sprintf("%d", v) + return strconv.Itoa(v) case float64: return fmt.Sprintf("%f", v) } diff --git a/pkg/util/version.go b/pkg/util/version.go index dda8fd14..96118b06 100644 --- a/pkg/util/version.go +++ b/pkg/util/version.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package util import ( diff --git a/pkg/util/version_compare.go b/pkg/util/version_compare.go index dc490081..c7e3d014 100644 --- a/pkg/util/version_compare.go +++ b/pkg/util/version_compare.go @@ -1,13 +1,13 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package util import "strconv" func compareVersionNumbers(left, right VersionInfo) int { - maxLen := len(left.Numbers) - if len(right.Numbers) > maxLen { - maxLen = len(right.Numbers) - } - for index := 0; index < maxLen; index++ { + maxLen := max(len(right.Numbers), len(left.Numbers)) + for index := range maxLen { leftValue := 0 rightValue := 0 if index < len(left.Numbers) { @@ -37,11 +37,8 @@ func compareGitDescribeDistance(left, right VersionInfo) int { } func compareGitDescribeTails(left, right VersionInfo) int { - maxLen := len(left.GitDescribeTail) - if len(right.GitDescribeTail) > maxLen { - maxLen = len(right.GitDescribeTail) - } - for index := 0; index < maxLen; index++ { + maxLen := max(len(right.GitDescribeTail), len(left.GitDescribeTail)) + for index := range maxLen { if index >= len(left.GitDescribeTail) { return -1 } @@ -69,11 +66,8 @@ func comparePrereleaseIdentifiers(left, right VersionInfo) int { return -1 } - maxLen := len(left.Prerelease) - if len(right.Prerelease) > maxLen { - maxLen = len(right.Prerelease) - } - for index := 0; index < maxLen; index++ { + maxLen := max(len(right.Prerelease), len(left.Prerelease)) + for index := range maxLen { if index >= len(left.Prerelease) { return -1 } diff --git a/pkg/wsclient/client.go b/pkg/wsclient/client.go index 467b539e..32ec5a70 100644 --- a/pkg/wsclient/client.go +++ b/pkg/wsclient/client.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package wsclient provides a WebSocket client for agent/server communication. package wsclient diff --git a/scripts/live_ch_smoke/main.go b/scripts/live_ch_smoke/main.go index e0ffa5a2..25655075 100644 --- a/scripts/live_ch_smoke/main.go +++ b/scripts/live_ch_smoke/main.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package main is a manual smoke tool for ClickHouse app write path. // Usage (from repo root, with config.yaml and Docker CH up): // @@ -6,6 +9,7 @@ package main import ( "context" + "errors" "fmt" "os" "time" @@ -32,7 +36,7 @@ func main() { func run() error { if !db.ChConnReady() { - return fmt.Errorf("ChConn not ready — check config.yaml clickhouse.enabled") + return errors.New("ChConn not ready — check config.yaml clickhouse.enabled") } ctx := context.Background() chwriter.Init(ctx) @@ -76,7 +80,7 @@ func waitForSnapshot(ctx context.Context, nodeID string, now time.Time) error { } time.Sleep(pollInterval) } - return fmt.Errorf("not flushed within timeout") + return errors.New("not flushed within timeout") } func assertLatestIncludes(ctx context.Context, nodeID string, now time.Time) error {