From 2f6032988637b4f9cf42a7e26cec5ca6cc15ceed Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 16 Aug 2026 21:23:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E7=AB=AF=E4=B8=8E=E5=85=A8=E4=BB=93?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=B4=A8=E9=87=8F=E6=B8=85=E7=90=86=EF=BC=88?= =?UTF-8?q?golangci=20=E6=89=A9=E5=B1=95=E9=9B=86=20=C2=B7=20=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E8=B4=A8=E9=87=8F=20=C2=B7=20=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E5=AE=89=E5=85=A8=20=C2=B7=20=E6=96=87=E6=A1=A3=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 代码质量全量清理,零行为变化:golangci 扩展集 13 类 linter(gosec/modernize/perfsprint/canonicalheader/usestdlibvars/wastedassign/intrange/errorlint/forcetypeassert/recvcheck/exhaustive/unparam)全量修复,测试代码质量(testifylint/thelper/usetesting)25→0,frpc 进程生命周期真 bug(进程组击杀)、全仓 go test -race 6 类数据竞争(含 1 个生产竞争)、SPDX license 头补齐 131 文件、前端测试套件 next-intl 迁移后 44 失败→全绿、过期 swagger 文档重新生成、pnpm-workspace 构建审批。 Experiments: #2-#17, #18, #20, #21, #23 Metric: total_issues 108 → 8 (-92.6%) --- cmd/agent/main.go | 6 +- cmd/agent/main_test.go | 3 + cmd/flared/main.go | 6 +- cmd/relay/main.go | 6 +- docs/docs.go | 11 ++- docs/swagger.json | 11 ++- docs/swagger.yaml | 8 ++- .../detail/components/pages-source-card.tsx | 2 +- frontend/messages/en.json | 2 +- frontend/messages/fragments/security.en.json | 2 +- .../messages/fragments/security.zh-CN.json | 2 +- frontend/messages/zh-CN.json | 2 +- frontend/pnpm-workspace.yaml | 2 + frontend/scripts/generate-themes.js | 2 +- .../cloudflare-group-detail.test.tsx | 10 ++- .../cloudflare/cloudflare-overview.test.tsx | 13 ++-- .../cloudflare/member-add-dialog.test.tsx | 34 ++++++---- .../cloudflare/sync-tasks-panel.test.tsx | 10 ++- .../pages-source-auto-update.test.tsx | 12 ++-- .../tests/openflare/pages-source-ui.test.tsx | 8 ++- .../tests/unit/waf-binding-order.test.tsx | 13 ++-- ...vice.test.ts => waf-rule-service.test.tsx} | 9 ++- frontend/tests/zone/websites-page.test.tsx | 10 ++- .../tests/zone/zone-domain-selector.test.tsx | 6 +- frontend/tests/zone/zone-page.test.tsx | 12 ++-- internal/apps/admin/db_manage/routers.go | 67 +++++++------------ internal/apps/admin/logs/routers.go | 4 +- internal/apps/admin/logs/utils.go | 12 ++-- internal/apps/admin/logs/utils_test.go | 1 + .../apps/admin/push/channels_definition.go | 10 +-- internal/apps/admin/push/events.go | 5 +- internal/apps/admin/push/push_test.go | 25 +++---- internal/apps/admin/status/routers.go | 5 +- internal/apps/admin/task/routers_test.go | 11 +-- internal/apps/admin/updater/logics.go | 17 ++--- internal/apps/admin/user/routers.go | 17 ++--- internal/apps/agent/agent/runner.go | 3 + internal/apps/agent/agent/runner_test.go | 3 + internal/apps/agent/config/config.go | 3 + internal/apps/agent/config/config_test.go | 3 + internal/apps/agent/config/duration.go | 3 + internal/apps/agent/config/version.go | 3 + internal/apps/agent/geoipdata/data.go | 3 + internal/apps/agent/geoipupdate/updater.go | 3 + .../apps/agent/geoipupdate/updater_test.go | 3 + internal/apps/agent/heartbeat/cycle.go | 3 + internal/apps/agent/heartbeat/service.go | 3 + internal/apps/agent/logging/logger.go | 3 + internal/apps/agent/nginx/manager.go | 15 +++-- internal/apps/agent/nginx/manager_test.go | 3 + internal/apps/agent/nginx/mimetypes.go | 3 + .../apps/agent/nginx/observability_assets.go | 3 + .../agent/nginx/observability_assets_test.go | 3 + internal/apps/agent/nginx/pow_assets.go | 3 + internal/apps/agent/nginx/pow_assets_test.go | 3 + internal/apps/agent/nginx/runtime_access.go | 3 + .../apps/agent/nginx/runtime_access_test.go | 3 + internal/apps/agent/nginx/sw_assets.go | 3 + internal/apps/agent/nginx/sw_assets_test.go | 3 + internal/apps/agent/nginx/waf_assets.go | 3 + internal/apps/agent/nginx/waf_assets_test.go | 3 + .../apps/agent/observability/collector.go | 3 + .../agent/observability/openresty_local.go | 5 +- .../observability/openresty_local_test.go | 3 + internal/apps/agent/observability/traffic.go | 3 + .../apps/agent/observability/traffic_test.go | 3 + internal/apps/agent/protocol/alias.go | 3 + .../apps/agent/runtimeuser/runtimeuser.go | 5 +- .../agent/runtimeuser/runtimeuser_test.go | 3 + .../agent/runtimeuser/runtimeuser_unix.go | 3 + .../apps/agent/state/observability_buffer.go | 3 + .../agent/state/observability_buffer_test.go | 3 + internal/apps/agent/state/state.go | 3 + internal/apps/agent/state/state_test.go | 3 + internal/apps/agent/sync/pages.go | 16 ++--- internal/apps/agent/sync/pages_stream_test.go | 12 +--- internal/apps/agent/sync/service.go | 6 +- internal/apps/agent/sync/sync_helpers.go | 6 ++ internal/apps/agent/updater/updater.go | 3 + internal/apps/agent/wsclient/client.go | 3 + internal/apps/cap/manager.go | 5 +- internal/apps/edge/config/duration.go | 3 + internal/apps/edge/config/duration_test.go | 3 + internal/apps/edge/heartbeat/autoupdate.go | 3 + internal/apps/edge/heartbeat/loop.go | 3 + internal/apps/edge/heartbeat/loop_test.go | 3 + internal/apps/edge/httpclient/client.go | 3 + internal/apps/edge/logging/logging.go | 3 + internal/apps/edge/nodeip/nodeip.go | 3 + internal/apps/edge/observability/linux.go | 32 ++++----- .../apps/edge/observability/linux_test.go | 3 + internal/apps/edge/runner/wsreconnect.go | 3 + internal/apps/edge/updater/restart_unix.go | 10 ++- .../apps/edge/updater/restart_unix_test.go | 3 + internal/apps/edge/updater/restart_windows.go | 3 + internal/apps/edge/updater/service.go | 10 ++- internal/apps/edge/updater/service_test.go | 3 + internal/apps/edge/wsclient/client.go | 3 + internal/apps/edge/wsclient/client_test.go | 3 + internal/apps/flared/config/config.go | 3 + internal/apps/flared/config/version.go | 3 + internal/apps/flared/flared/runner.go | 3 + internal/apps/flared/frpc/manager.go | 16 ++++- internal/apps/flared/frpc/manager_test.go | 32 ++++++++- internal/apps/flared/heartbeat/service.go | 3 + internal/apps/flared/httpclient/client.go | 3 + internal/apps/flared/sync/service.go | 3 + internal/apps/flared/updater/updater.go | 3 + internal/apps/flared/wsclient/client.go | 3 + internal/apps/oauth/auth_source_resolver.go | 8 +-- internal/apps/oauth/cache.go | 31 +++++++-- internal/apps/oauth/oauth_test.go | 6 ++ .../apps/openflare/agent/observability.go | 15 +---- internal/apps/openflare/agent/waf_ip_group.go | 7 +- .../apps/openflare/apply_log/logics_test.go | 1 + internal/apps/openflare/async_tasks.go | 3 + internal/apps/openflare/cloudflare/client.go | 2 +- .../certificate_snapshot_test.go | 1 + .../apps/openflare/config_version/helpers.go | 13 ++-- .../apps/openflare/config_version/logics.go | 52 +++++++------- .../origin_error_page_snapshot_test.go | 2 +- .../apps/openflare/config_version/snapshot.go | 11 ++- .../apps/openflare/dashboard/logics_test.go | 17 ++--- internal/apps/openflare/flared/logics.go | 4 +- internal/apps/openflare/flared/routers.go | 22 ++++-- internal/apps/openflare/geoip/data/data.go | 3 + .../apps/openflare/geoip/data/data_test.go | 3 + internal/apps/openflare/geoip/runtime.go | 15 +++-- internal/apps/openflare/geoip/runtime_test.go | 3 + .../openflare/integration/core_chain_test.go | 8 +-- .../openflare/integration/security_test.go | 12 ++-- internal/apps/openflare/node/helpers.go | 23 ++++--- internal/apps/openflare/node/logics.go | 6 +- .../access_log_ip_summary_test.go | 3 + .../observability/access_log_logics.go | 5 +- internal/apps/openflare/option/logics.go | 4 +- .../openflare/option/openresty_validators.go | 4 +- internal/apps/openflare/option/routers.go | 5 +- internal/apps/openflare/option/validate.go | 12 ++-- internal/apps/openflare/origin/helpers.go | 4 +- internal/apps/openflare/pages/helpers.go | 15 ++--- internal/apps/openflare/pages/logics.go | 2 +- internal/apps/openflare/pages/rebind.go | 17 ++--- .../openflare/pages/source_orphan_cleanup.go | 2 + .../apps/openflare/pages/source_scanner.go | 2 + internal/apps/openflare/pages/source_sync.go | 4 +- internal/apps/openflare/pages/source_tasks.go | 8 +-- .../apps/openflare/proxy_route/helpers.go | 8 +-- internal/apps/openflare/proxy_route/logics.go | 44 ++++++------ .../apps/openflare/proxy_route/logics_test.go | 4 +- internal/apps/openflare/relay/logics.go | 3 +- internal/apps/openflare/relay/logics_test.go | 2 +- internal/apps/openflare/relay/routers.go | 12 +++- .../apps/openflare/tasks/ssl_renew_test.go | 26 ++++--- internal/apps/openflare/tls/acme/client.go | 2 +- .../apps/openflare/tls/acme_obtain_test.go | 14 +++- internal/apps/openflare/tls/logics.go | 19 ++++-- internal/apps/openflare/tls/obtain.go | 2 +- internal/apps/openflare/uptimekuma/client.go | 14 ++-- internal/apps/openflare/uptimekuma/sync.go | 27 +++----- .../apps/openflare/uptimekuma/sync_helpers.go | 9 +-- internal/apps/openflare/waf/graph_compile.go | 8 ++- internal/apps/openflare/waf/graph_validate.go | 12 ++-- internal/apps/openflare/waf/ip_group_sync.go | 9 ++- .../apps/openflare/waf/rule_logics_test.go | 8 ++- internal/apps/openflare/zone/legacy_import.go | 6 +- internal/apps/openflare/zone/stats.go | 5 +- internal/apps/relay/config/config.go | 3 + internal/apps/relay/config/version.go | 3 + internal/apps/relay/frps/manager.go | 5 +- internal/apps/relay/frps/manager_test.go | 28 +++++--- internal/apps/relay/heartbeat/service.go | 3 + internal/apps/relay/httpclient/client.go | 3 + .../apps/relay/observability/collector.go | 3 + internal/apps/relay/relay/runner.go | 3 + internal/apps/relay/state/store.go | 3 + internal/apps/relay/updater/updater.go | 3 + internal/apps/relay/wsclient/client.go | 3 + internal/apps/upload/cache/access_cache.go | 5 +- internal/apps/upload/filesrv/file_server.go | 10 ++- .../apps/upload/filesrv/file_server_test.go | 1 + .../apps/upload/handler/file_management.go | 2 +- internal/apps/upload/handler/routers.go | 5 +- internal/apps/upload/handler/routers_test.go | 2 + internal/apps/upload/ingest/helpers.go | 4 +- internal/apps/upload/ingest/ingest.go | 2 + internal/apps/upload/ingest/ingest_test.go | 1 + .../apps/upload/task/storage_migration.go | 18 ++--- internal/apps/upload/util/media.go | 22 ++---- internal/apps/user/tasks.go | 2 +- internal/cmd/all.go | 18 ++--- internal/cmd/banner.go | 14 ++-- internal/infra/config/config.go | 13 ++-- internal/infra/config/config_test.go | 3 + internal/infra/objectstore/local.go | 7 +- .../infra/persistence/batchwriter/config.go | 12 ++-- .../migrator/pages_source_migration_test.go | 6 +- internal/infra/persistence/postgres.go | 10 ++- internal/infra/persistence/postgres_logger.go | 10 +-- internal/infra/persistence/redis.go | 3 +- internal/infra/persistence/redis_test.go | 3 + internal/infra/task/executor.go | 2 +- internal/infra/task/executor_test.go | 11 +-- internal/infra/task/permanent_error_test.go | 8 +-- internal/infra/task/utils.go | 6 +- internal/infra/task/utils_test.go | 3 + internal/integration/githubrelease/client.go | 17 ++--- .../integration/githubrelease/transport.go | 8 +-- internal/model/access_token.go | 2 +- internal/model/auth_source.go | 2 +- internal/model/openflare_config_version.go | 2 +- internal/model/push_channel.go | 2 +- internal/model/push_event.go | 2 +- internal/model/templates.go | 2 +- internal/model/users.go | 2 +- internal/repository/analytics/access_log.go | 3 +- .../repository/analytics/access_log_stats.go | 2 +- .../repository/analytics/access_log_writer.go | 3 +- .../repository/analytics/clickhouse_stats.go | 3 +- .../repository/analytics/node_access_log.go | 3 +- .../analytics/node_access_log_test.go | 4 +- .../analytics/node_access_log_writer.go | 3 +- .../analytics/node_observability.go | 7 +- .../node_observability_latest_test.go | 14 ++-- .../analytics/node_observability_writer.go | 9 +-- internal/repository/auth_source_cache.go | 13 +++- .../repository/logstore/postgres_store.go | 2 +- internal/repository/openflare_access_log.go | 10 +-- .../repository/openflare_apply_log_test.go | 3 + internal/repository/openflare_zone.go | 6 +- internal/repository/system_config_cache.go | 13 +++- internal/repository/task_execution.go | 3 + internal/repository/task_execution_test.go | 5 +- internal/router/middlewares.go | 8 +-- internal/shared/response/middleware_test.go | 2 +- internal/shared/response/response.go | 4 +- internal/testhelper/test_helper.go | 2 + pkg/cache/disk/cache.go | 35 ++++++++-- pkg/cache/ram/manager.go | 5 +- pkg/cap/cap.go | 6 +- pkg/cap/prng.go | 4 +- pkg/cap/store.go | 24 ++++--- pkg/geoip/country_centroid.go | 5 +- pkg/geoip/country_centroid_test.go | 3 + pkg/geoip/emptyProvider.go | 9 ++- pkg/geoip/geoip.go | 8 ++- pkg/geoip/geoip_test.go | 3 + pkg/geoip/geojs.go | 3 + pkg/geoip/ipapi.go | 3 + pkg/geoip/ipinfo.go | 3 + pkg/geoip/iputil/iputil.go | 3 + pkg/geoip/iputil/iputil_test.go | 3 + pkg/geoip/mmdb.go | 8 ++- pkg/geoip/outboundip.go | 3 + pkg/geoip/outboundip_test.go | 3 + pkg/logger/logger.go | 8 +-- pkg/logger/ringbuffer.go | 9 +-- pkg/logger/ringbuffer_test.go | 25 +++---- pkg/mail/mail.go | 20 +++--- pkg/pagesarchive/entry.go | 4 +- pkg/pagesarchive/extract.go | 8 +-- pkg/pagesarchive/format.go | 4 +- pkg/pagesarchive/inspect.go | 10 +-- pkg/pagesarchive/list.go | 7 +- pkg/pagesarchive/path.go | 11 +-- pkg/protocol/server.go | 3 + pkg/push/custom_test.go | 2 +- pkg/push/email.go | 2 +- pkg/push/lark.go | 4 +- pkg/push/telegram.go | 2 +- pkg/push/telegram_test.go | 10 +-- pkg/render/openresty/origin_error_page.go | 3 + .../openresty/origin_error_page_test.go | 3 + pkg/render/openresty/render.go | 42 ++++++------ pkg/render/openresty/render_route.go | 3 + pkg/render/openresty/render_test.go | 8 +-- pkg/render/openresty/service_worker.go | 3 + pkg/render/openresty/service_worker_test.go | 3 + pkg/render/openresty/status_codes.go | 9 +-- pkg/render/openresty/status_codes_test.go | 3 + pkg/render/openresty/types.go | 3 + pkg/util/format.go | 11 +-- pkg/util/format_test.go | 3 + pkg/util/network.go | 3 + pkg/util/network_test.go | 3 + pkg/util/slice.go | 3 + pkg/util/string.go | 3 + pkg/util/value.go | 12 +++- pkg/util/version.go | 3 + pkg/util/version_compare.go | 24 +++---- pkg/wsclient/client.go | 3 + scripts/live_ch_smoke/main.go | 8 ++- 292 files changed, 1362 insertions(+), 793 deletions(-) rename frontend/tests/unit/{waf-rule-service.test.ts => waf-rule-service.test.tsx} (94%) diff --git a/cmd/agent/main.go b/cmd/agent/main.go index 73bc2a2f..5c6842ee 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -1,8 +1,12 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Command agent runs the OpenFlare edge agent daemon. package main import ( "context" + "errors" "flag" "log/slog" "os" @@ -132,7 +136,7 @@ func main() { go geoIPUpdater.Run(ctx) slog.Info("agent process started") - if err = runner.Run(ctx); err != nil && err != context.Canceled { + if err = runner.Run(ctx); err != nil && !errors.Is(err, context.Canceled) { slog.Error("agent process exited with error", "error", err) stop() os.Exit(1) diff --git a/cmd/agent/main_test.go b/cmd/agent/main_test.go index 2a99adf9..98a34474 100644 --- a/cmd/agent/main_test.go +++ b/cmd/agent/main_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package main import ( diff --git a/cmd/flared/main.go b/cmd/flared/main.go index 679d1132..7425e14c 100644 --- a/cmd/flared/main.go +++ b/cmd/flared/main.go @@ -1,8 +1,12 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Command flared runs the OpenFlare tunnel client daemon. package main import ( "context" + "errors" "flag" "log/slog" "os" @@ -63,7 +67,7 @@ func main() { slog.Info("flared process started") - if err := runner.Run(ctx); err != nil && err != context.Canceled { + if err := runner.Run(ctx); err != nil && !errors.Is(err, context.Canceled) { slog.Error("flared process exited with error", "error", err) stop() os.Exit(1) diff --git a/cmd/relay/main.go b/cmd/relay/main.go index 083aba2a..d180cf00 100644 --- a/cmd/relay/main.go +++ b/cmd/relay/main.go @@ -1,8 +1,12 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Command relay runs the OpenFlare relay node daemon. package main import ( "context" + "errors" "flag" "log/slog" "os" @@ -63,7 +67,7 @@ func main() { slog.Info("relay process started") - if err := runner.Run(ctx); err != nil && err != context.Canceled { + if err := runner.Run(ctx); err != nil && !errors.Is(err, context.Canceled) { slog.Error("relay process exited with error", "error", err) stop() os.Exit(1) diff --git a/docs/docs.go b/docs/docs.go index cc850e20..ec7252b7 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -15582,7 +15582,7 @@ const docTemplate = `{ "type": "array", "items": { "type": "object", - "additionalProperties": true + "additionalProperties": {} } }, "type": { @@ -18403,6 +18403,15 @@ const docTemplate = `{ "request_count": { "type": "integer" }, + "status_2xx_count": { + "type": "integer" + }, + "status_4xx_count": { + "type": "integer" + }, + "status_5xx_count": { + "type": "integer" + }, "unique_visitor_count": { "type": "integer" } diff --git a/docs/swagger.json b/docs/swagger.json index 578b1059..798c9a09 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -15575,7 +15575,7 @@ "type": "array", "items": { "type": "object", - "additionalProperties": true + "additionalProperties": {} } }, "type": { @@ -18396,6 +18396,15 @@ "request_count": { "type": "integer" }, + "status_2xx_count": { + "type": "integer" + }, + "status_4xx_count": { + "type": "integer" + }, + "status_5xx_count": { + "type": "integer" + }, "unique_visitor_count": { "type": "integer" } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 5f025236..def997e1 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -645,7 +645,7 @@ definitions: type: integer results: items: - additionalProperties: true + additionalProperties: {} type: object type: array type: @@ -2515,6 +2515,12 @@ definitions: type: integer request_count: type: integer + status_2xx_count: + type: integer + status_4xx_count: + type: integer + status_5xx_count: + type: integer unique_visitor_count: type: integer type: object diff --git a/frontend/app/(main)/pages/detail/components/pages-source-card.tsx b/frontend/app/(main)/pages/detail/components/pages-source-card.tsx index d9860b75..2c9df411 100644 --- a/frontend/app/(main)/pages/detail/components/pages-source-card.tsx +++ b/frontend/app/(main)/pages/detail/components/pages-source-card.tsx @@ -272,7 +272,7 @@ export function PagesSourceCard({ projectId }: { projectId: number }) { } setActiveAction(null); setActionTimedOut(false); - }, [activeAction, executionQuery.data, invalidateSourceState]); + }, [activeAction, executionQuery.data, invalidateSourceState, t]); const checkMutation = useMutation({ mutationFn: () => PagesService.checkSource(projectId), diff --git a/frontend/messages/en.json b/frontend/messages/en.json index d31be8b9..52faa90e 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -3243,7 +3243,7 @@ "switchFailed": "Switch failed", "urlRequired": "Enter a Remote URL", "urlInvalid": "Enter a valid HTTP(S) URL", - "githubUrlInvalid": "Enter a public repository URL in https://github.com/{owner}/{repo} format", + "githubUrlInvalid": "Enter a public repository URL in https://github.com/'{owner}'/'{repo}' format", "tagInvalid": "Release tag must be a valid Git ref (1–255 bytes; / # & = are allowed)", "assetInvalid": "Asset name must be 1–255 bytes and cannot be a path or contain control, newline, or bidi characters", "intervalInvalid": "Check interval must be an integer between 5 and 1440 minutes", diff --git a/frontend/messages/fragments/security.en.json b/frontend/messages/fragments/security.en.json index febef139..7854d61d 100644 --- a/frontend/messages/fragments/security.en.json +++ b/frontend/messages/fragments/security.en.json @@ -499,7 +499,7 @@ "switchFailed": "Switch failed", "urlRequired": "Enter a Remote URL", "urlInvalid": "Enter a valid HTTP(S) URL", - "githubUrlInvalid": "Enter a public repository URL in https://github.com/{owner}/{repo} format", + "githubUrlInvalid": "Enter a public repository URL in https://github.com/'{owner}'/'{repo}' format", "tagInvalid": "Release tag must be a valid Git ref (1–255 bytes; / # & = are allowed)", "assetInvalid": "Asset name must be 1–255 bytes and cannot be a path or contain control, newline, or bidi characters", "intervalInvalid": "Check interval must be an integer between 5 and 1440 minutes", diff --git a/frontend/messages/fragments/security.zh-CN.json b/frontend/messages/fragments/security.zh-CN.json index fabd8458..12ac4acc 100644 --- a/frontend/messages/fragments/security.zh-CN.json +++ b/frontend/messages/fragments/security.zh-CN.json @@ -499,7 +499,7 @@ "switchFailed": "切换失败", "urlRequired": "请输入 Remote URL", "urlInvalid": "请输入有效的 HTTP(S) URL", - "githubUrlInvalid": "请输入 https://github.com/{owner}/{repo} 格式的公开仓库地址", + "githubUrlInvalid": "请输入 https://github.com/'{owner}'/'{repo}' 格式的公开仓库地址", "tagInvalid": "Release tag 须为有效 Git ref(1–255 字节,可使用 /、#、&、=)", "assetInvalid": "Asset 文件名须为 1–255 字节,且不能是路径或包含控制、换行、双向文本字符", "intervalInvalid": "检查间隔须为 5–1440 分钟的整数", diff --git a/frontend/messages/zh-CN.json b/frontend/messages/zh-CN.json index 1d605168..fb433f07 100644 --- a/frontend/messages/zh-CN.json +++ b/frontend/messages/zh-CN.json @@ -3243,7 +3243,7 @@ "switchFailed": "切换失败", "urlRequired": "请输入 Remote URL", "urlInvalid": "请输入有效的 HTTP(S) URL", - "githubUrlInvalid": "请输入 https://github.com/{owner}/{repo} 格式的公开仓库地址", + "githubUrlInvalid": "请输入 https://github.com/'{owner}'/'{repo}' 格式的公开仓库地址", "tagInvalid": "Release tag 须为有效 Git ref(1–255 字节,可使用 /、#、&、=)", "assetInvalid": "Asset 文件名须为 1–255 字节,且不能是路径或包含控制、换行、双向文本字符", "intervalInvalid": "检查间隔须为 5–1440 分钟的整数", diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index 07399aa0..42ea1c72 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -1,4 +1,6 @@ allowBuilds: + '@parcel/watcher': true + '@swc/core': true core-js: true sharp: true unrs-resolver: true diff --git a/frontend/scripts/generate-themes.js b/frontend/scripts/generate-themes.js index ec36ce7f..60e0fed6 100644 --- a/frontend/scripts/generate-themes.js +++ b/frontend/scripts/generate-themes.js @@ -68,7 +68,7 @@ try { fs.mkdirSync(outputDir, { recursive: true }); } - fs.writeFileSync(OUTPUT_FILE, JSON.stringify(themes, null, 2), 'utf-8'); + fs.writeFileSync(OUTPUT_FILE, JSON.stringify(themes, null, 2) + '\n', 'utf-8'); console.log(`Successfully generated themes.json at ${OUTPUT_FILE}`); } catch (error) { console.error('Failed to generate themes.json:', error); diff --git a/frontend/tests/cloudflare/cloudflare-group-detail.test.tsx b/frontend/tests/cloudflare/cloudflare-group-detail.test.tsx index f6b644b4..cde716bc 100644 --- a/frontend/tests/cloudflare/cloudflare-group-detail.test.tsx +++ b/frontend/tests/cloudflare/cloudflare-group-detail.test.tsx @@ -10,6 +10,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { CloudflareGroupDetailPageClient } from '@/app/(main)/cloudflare/groups/[id]/page-client'; import { CloudflareService, NodeService } from '@/lib/services/openflare'; +import { NextIntlClientProvider } from 'next-intl'; +import zhCN from '@/messages/zh-CN.json'; vi.mock('next/navigation', () => ({ useParams: () => ({ id: '7' }), @@ -39,9 +41,11 @@ function renderPage() { defaultOptions: { queries: { retry: false, gcTime: 0 } }, }); render( - - - , + + + + + , ); } diff --git a/frontend/tests/cloudflare/cloudflare-overview.test.tsx b/frontend/tests/cloudflare/cloudflare-overview.test.tsx index e4d75e45..9e0c20b8 100644 --- a/frontend/tests/cloudflare/cloudflare-overview.test.tsx +++ b/frontend/tests/cloudflare/cloudflare-overview.test.tsx @@ -1,9 +1,10 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, screen } from '@testing-library/react'; -import { createElement } from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import CloudflarePage from '@/app/(main)/cloudflare/page'; +import { NextIntlClientProvider } from 'next-intl'; +import zhCN from '@/messages/zh-CN.json'; import { AdminTaskService } from '@/lib/services/admin'; import { CloudflareService, NodeService } from '@/lib/services/openflare'; @@ -42,11 +43,11 @@ function renderPage() { defaultOptions: { queries: { retry: false, gcTime: 0 } }, }); render( - createElement( - QueryClientProvider, - { client }, - createElement(CloudflarePage), - ), + + + + + , ); } diff --git a/frontend/tests/cloudflare/member-add-dialog.test.tsx b/frontend/tests/cloudflare/member-add-dialog.test.tsx index 757e58bc..a3d804f3 100644 --- a/frontend/tests/cloudflare/member-add-dialog.test.tsx +++ b/frontend/tests/cloudflare/member-add-dialog.test.tsx @@ -2,6 +2,8 @@ import { fireEvent, render, screen, within } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { MemberAddDialog } from '@/app/(main)/cloudflare/components/member-add-dialog'; +import { NextIntlClientProvider } from 'next-intl'; +import zhCN from '@/messages/zh-CN.json'; import type { CloudflareAvailableDomain } from '@/lib/services/openflare'; const domains: CloudflareAvailableDomain[] = [ @@ -35,15 +37,17 @@ describe('MemberAddDialog advanced domain picker', () => { it('groups domains under top-level zone roots and supports batch select', () => { const onSubmit = vi.fn(); render( - + , - ); + /> + , + ); expect(screen.getAllByText('example.com').length).toBeGreaterThan(0); expect(screen.getAllByText('other.io').length).toBeGreaterThan(0); @@ -61,15 +65,17 @@ describe('MemberAddDialog advanced domain picker', () => { it('filters by keyword across domain and zone root', () => { render( - + , - ); + /> + , + ); fireEvent.change(screen.getByPlaceholderText('搜索域名或顶级域…'), { target: { value: 'other' }, @@ -83,15 +89,17 @@ describe('MemberAddDialog advanced domain picker', () => { it('selects an entire top-level domain group', () => { const onSubmit = vi.fn(); render( - + , - ); + /> + , + ); fireEvent.click( screen.getByRole('checkbox', { name: '选择顶级域 example.com' }), @@ -107,15 +115,17 @@ describe('MemberAddDialog advanced domain picker', () => { it('toggles individual domains within a group', () => { const onSubmit = vi.fn(); render( - + , - ); + /> + , + ); const wwwLabel = screen.getByText('www.example.com').closest('label'); expect(wwwLabel).toBeTruthy(); diff --git a/frontend/tests/cloudflare/sync-tasks-panel.test.tsx b/frontend/tests/cloudflare/sync-tasks-panel.test.tsx index a071f03d..70173f4d 100644 --- a/frontend/tests/cloudflare/sync-tasks-panel.test.tsx +++ b/frontend/tests/cloudflare/sync-tasks-panel.test.tsx @@ -5,6 +5,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { SyncTasksPanel } from '@/app/(main)/cloudflare/components/sync-tasks-panel'; import { AdminTaskService } from '@/lib/services/admin'; import type { TaskExecution } from '@/lib/services/admin'; +import { NextIntlClientProvider } from 'next-intl'; +import zhCN from '@/messages/zh-CN.json'; vi.mock('@/lib/services/admin', async (importOriginal) => { const actual = await importOriginal(); @@ -24,9 +26,11 @@ function renderPanel() { defaultOptions: { queries: { retry: false, gcTime: 0 } }, }); render( - - - , + + + + + , ); } diff --git a/frontend/tests/openflare/pages-source-auto-update.test.tsx b/frontend/tests/openflare/pages-source-auto-update.test.tsx index a1e65d6f..079a47b3 100644 --- a/frontend/tests/openflare/pages-source-auto-update.test.tsx +++ b/frontend/tests/openflare/pages-source-auto-update.test.tsx @@ -20,6 +20,8 @@ import { type PagesGitHubReleaseSource, PagesService, } from '@/lib/services/openflare'; +import { NextIntlClientProvider } from 'next-intl'; +import zhCN from '@/messages/zh-CN.json'; vi.mock('@/lib/services/openflare', async (importOriginal) => { const actual = @@ -48,16 +50,18 @@ function renderWithQuery(ui: React.ReactNode) { }, }); const rendered = render( - {ui}, + + {ui} + , ); return { ...rendered, queryClient, rerenderWithQuery: (nextUI: React.ReactNode) => rendered.rerender( - - {nextUI} - , + + {nextUI} + , ), }; } diff --git a/frontend/tests/openflare/pages-source-ui.test.tsx b/frontend/tests/openflare/pages-source-ui.test.tsx index 934754d8..69b38bd8 100644 --- a/frontend/tests/openflare/pages-source-ui.test.tsx +++ b/frontend/tests/openflare/pages-source-ui.test.tsx @@ -17,6 +17,8 @@ import { type PagesRemoteURLSource, PagesService, } from '@/lib/services/openflare'; +import { NextIntlClientProvider } from 'next-intl'; +import zhCN from '@/messages/zh-CN.json'; vi.mock('@/lib/services/openflare', async (importOriginal) => { const actual = @@ -56,7 +58,9 @@ function renderWithQuery(ui: React.ReactNode) { }, }); return render( - {ui}, + + {ui} + , ); } @@ -390,7 +394,7 @@ describe('Pages source UI', () => { expect(screen.getByText('下次检查')).toBeVisible(); expect(screen.getByText('自动更新')).toBeVisible(); expect(screen.getByText('已关闭')).toBeVisible(); - expect(screen.getByText('检查间隔')).toBeVisible(); + expect(screen.getByText('检查间隔(分钟)')).toBeVisible(); expect(screen.getByText('1440 分钟')).toBeVisible(); }); diff --git a/frontend/tests/unit/waf-binding-order.test.tsx b/frontend/tests/unit/waf-binding-order.test.tsx index 76013c3c..c2d9f90b 100644 --- a/frontend/tests/unit/waf-binding-order.test.tsx +++ b/frontend/tests/unit/waf-binding-order.test.tsx @@ -1,6 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { createElement } from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { @@ -9,6 +8,8 @@ import { } from '@/app/(main)/proxy-routes/detail/components/waf-section'; import type { ProxyRouteItem, WAFRule } from '@/lib/services/openflare'; import { WafService } from '@/lib/services/openflare'; +import { NextIntlClientProvider } from 'next-intl'; +import zhCN from '@/messages/zh-CN.json'; vi.mock('@/lib/services/openflare', async (importOriginal) => { const actual = @@ -97,11 +98,11 @@ describe('WAF route binding order', () => { defaultOptions: { queries: { retry: false } }, }); render( - createElement( - QueryClientProvider, - { client }, - createElement(WafSection, { route: { id: 9 } as ProxyRouteItem }), - ), + + + + + , ); expect(await screen.findByText('全局规则')).toBeInTheDocument(); diff --git a/frontend/tests/unit/waf-rule-service.test.ts b/frontend/tests/unit/waf-rule-service.test.tsx similarity index 94% rename from frontend/tests/unit/waf-rule-service.test.ts rename to frontend/tests/unit/waf-rule-service.test.tsx index cd9f55a0..8352c922 100644 --- a/frontend/tests/unit/waf-rule-service.test.ts +++ b/frontend/tests/unit/waf-rule-service.test.tsx @@ -1,7 +1,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { NextIntlClientProvider } from 'next-intl'; +import zhCN from '@/messages/zh-CN.json'; import type { AxiosResponse } from 'axios'; -import { createElement } from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import WafPage from '@/app/(main)/waf/page'; @@ -156,7 +157,11 @@ describe('WAF rule creation flow', () => { defaultOptions: { queries: { retry: false, gcTime: 0 } }, }); render( - createElement(QueryClientProvider, { client }, createElement(WafPage)), + + + + + , ); fireEvent.click(await screen.findByRole('button', { name: '新建规则' })); diff --git a/frontend/tests/zone/websites-page.test.tsx b/frontend/tests/zone/websites-page.test.tsx index 489b72a3..72f0beb1 100644 --- a/frontend/tests/zone/websites-page.test.tsx +++ b/frontend/tests/zone/websites-page.test.tsx @@ -4,6 +4,8 @@ import { describe, expect, it, vi } from 'vitest'; import WebsitesPage from '@/app/(main)/websites/page'; import { ZoneService } from '@/lib/services/openflare'; +import { NextIntlClientProvider } from 'next-intl'; +import zhCN from '@/messages/zh-CN.json'; vi.mock('next/link', () => ({ default: ({ @@ -30,9 +32,11 @@ function renderPage() { defaultOptions: { queries: { retry: false } }, }); return render( - - - , + + + + + , ); } diff --git a/frontend/tests/zone/zone-domain-selector.test.tsx b/frontend/tests/zone/zone-domain-selector.test.tsx index aafbaa3a..992bea4e 100644 --- a/frontend/tests/zone/zone-domain-selector.test.tsx +++ b/frontend/tests/zone/zone-domain-selector.test.tsx @@ -5,6 +5,8 @@ import { describe, expect, it, vi } from 'vitest'; import { ZoneDomainSelector } from '@/app/(main)/proxy-routes/components/zone-domain-selector'; import type { ZoneDomainItem, ZoneItem } from '@/lib/services/openflare'; +import { NextIntlClientProvider } from 'next-intl'; +import zhCN from '@/messages/zh-CN.json'; vi.mock('next/link', () => ({ default: ({ @@ -46,7 +48,9 @@ function renderSelector(ui: React.ReactElement) { defaultOptions: { queries: { retry: false } }, }); return render( - {ui}, + + {ui} + , ); } diff --git a/frontend/tests/zone/zone-page.test.tsx b/frontend/tests/zone/zone-page.test.tsx index f792be2a..3649b9a3 100644 --- a/frontend/tests/zone/zone-page.test.tsx +++ b/frontend/tests/zone/zone-page.test.tsx @@ -8,6 +8,8 @@ import { TlsCertificateService, ZoneService, } from '@/lib/services/openflare'; +import { NextIntlClientProvider } from 'next-intl'; +import zhCN from '@/messages/zh-CN.json'; class ResizeObserverMock { observe() {} @@ -68,9 +70,11 @@ function renderPage(zoneId: number, paramZoneId = zoneId) { }, }); return render( - - - , + + + + + , ); } @@ -113,7 +117,7 @@ describe('ZonePageClient', () => { expect( await screen.findByRole('heading', { name: 'example.com' }), ).toBeVisible(); - expect(await screen.findByText('唯一访问者')).toBeVisible(); + expect(await screen.findByText('查询窗口独立访客')).toBeVisible(); expect(screen.getByText('请求总数')).toBeVisible(); expect(screen.getByText('已提供的数据总计')).toBeVisible(); expect(screen.getByRole('tab', { name: '域名 (0)' })).toBeVisible(); diff --git a/internal/apps/admin/db_manage/routers.go b/internal/apps/admin/db_manage/routers.go index a6381fbb..04fe6b6d 100644 --- a/internal/apps/admin/db_manage/routers.go +++ b/internal/apps/admin/db_manage/routers.go @@ -50,9 +50,9 @@ type GetTableDataRequest struct { // TableDataResponse 动态数据表响应结构体 type TableDataResponse struct { - Columns []string `json:"columns"` - Total int64 `json:"total"` - Results []map[string]interface{} `json:"results"` + Columns []string `json:"columns"` + Total int64 `json:"total"` + Results []map[string]any `json:"results"` } // ExecuteSQLRequest 执行自定义 SQL 请求结构体 @@ -62,11 +62,11 @@ type ExecuteSQLRequest struct { // ExecuteSQLResponse 执行自定义 SQL 响应结构体 type ExecuteSQLResponse struct { - Type string `json:"type"` // "select" 或 "exec" - Columns []string `json:"columns,omitempty"` - Results []map[string]interface{} `json:"results,omitempty"` - AffectedRows int64 `json:"affected_rows"` - ExecutionTimeMs int64 `json:"execution_time_ms"` + Type string `json:"type"` // "select" 或 "exec" + Columns []string `json:"columns,omitempty"` + Results []map[string]any `json:"results,omitempty"` + AffectedRows int64 `json:"affected_rows"` + ExecutionTimeMs int64 `json:"execution_time_ms"` } // formatBytes 格式化字节大小为可读字符串 @@ -103,7 +103,7 @@ func formatBytes(bytes uint64) string { } // getSQLiteOverview 获取 SQLite 数据库概览信息 -func getSQLiteOverview(gormDB *gorm.DB) (DBOverviewResponse, error) { +func getSQLiteOverview(gormDB *gorm.DB) DBOverviewResponse { name := config.Config.Database.SQLitePath if name == "" { name = "./data/openflare.db" @@ -119,10 +119,7 @@ func getSQLiteOverview(gormDB *gorm.DB) (DBOverviewResponse, error) { var sizeStr string if fi, err := os.Stat(name); err == nil { - size := fi.Size() - if size < 0 { - size = 0 - } + size := max(fi.Size(), 0) sizeStr = formatBytes(uint64(size)) } else { sizeStr = "0 B" @@ -147,11 +144,11 @@ func getSQLiteOverview(gormDB *gorm.DB) (DBOverviewResponse, error) { Size: sizeStr, TableCount: tableCount, Connections: connCount, - }, nil + } } // getPostgresOverview 获取 PostgreSQL 数据库概览信息 -func getPostgresOverview(gormDB *gorm.DB) (DBOverviewResponse, error) { +func getPostgresOverview(gormDB *gorm.DB) DBOverviewResponse { name := config.Config.Database.Database var version string @@ -165,10 +162,7 @@ func getPostgresOverview(gormDB *gorm.DB) (DBOverviewResponse, error) { var sizeStr string var sizeBytes sql.NullInt64 if err := gormDB.Raw("SELECT pg_database_size(current_database())").Scan(&sizeBytes).Error; err == nil && sizeBytes.Valid { - size := sizeBytes.Int64 - if size < 0 { - size = 0 - } + size := max(sizeBytes.Int64, 0) sizeStr = formatBytes(uint64(size)) } else { sizeStr = "0 B" @@ -198,7 +192,7 @@ func getPostgresOverview(gormDB *gorm.DB) (DBOverviewResponse, error) { Size: sizeStr, TableCount: tableCount, Connections: connCount, - }, nil + } } // GetDBOverview 获取数据库运行概览 @@ -220,17 +214,11 @@ func GetDBOverview(c *gin.Context) { } var overview DBOverviewResponse - var err error if !config.Config.Database.Enabled { - overview, err = getSQLiteOverview(gormDB) + overview = getSQLiteOverview(gormDB) } else { - overview, err = getPostgresOverview(gormDB) - } - - if err != nil { - response.AbortInternal(c, err.Error()) - return + overview = getPostgresOverview(gormDB) } c.JSON(http.StatusOK, response.OK(overview)) @@ -294,10 +282,7 @@ func GetDBTableData(c *gin.Context) { return } - offset := (req.Page - 1) * req.PageSize - if offset < 0 { - offset = 0 - } + offset := max((req.Page-1)*req.PageSize, 0) limit := req.PageSize if limit <= 0 { limit = 10 @@ -332,11 +317,11 @@ func GetDBTableData(c *gin.Context) { } // scanTableRows 扫描并提取数据表行数据,做截断处理 -func scanTableRows(rows *sql.Rows, cols []string) ([]map[string]interface{}, error) { - results := make([]map[string]interface{}, 0) +func scanTableRows(rows *sql.Rows, cols []string) ([]map[string]any, error) { + results := make([]map[string]any, 0) for rows.Next() { - columns := make([]interface{}, len(cols)) - columnPointers := make([]interface{}, len(cols)) + columns := make([]any, len(cols)) + columnPointers := make([]any, len(cols)) for i := range columns { columnPointers[i] = &columns[i] } @@ -345,7 +330,7 @@ func scanTableRows(rows *sql.Rows, cols []string) ([]map[string]interface{}, err return nil, err } - rowMap := make(map[string]interface{}) + rowMap := make(map[string]any) for i, colName := range cols { val := columns[i] if b, ok := val.([]byte); ok { @@ -385,10 +370,10 @@ func executeSQLQuery(gormDB *gorm.DB, sqlStr string, startTime time.Time) (Execu return ExecuteSQLResponse{}, err } - results := make([]map[string]interface{}, 0) + results := make([]map[string]any, 0) for rows.Next() { - columns := make([]interface{}, len(cols)) - columnPointers := make([]interface{}, len(cols)) + columns := make([]any, len(cols)) + columnPointers := make([]any, len(cols)) for i := range columns { columnPointers[i] = &columns[i] } @@ -397,7 +382,7 @@ func executeSQLQuery(gormDB *gorm.DB, sqlStr string, startTime time.Time) (Execu return ExecuteSQLResponse{}, err } - rowMap := make(map[string]interface{}) + rowMap := make(map[string]any) for i, colName := range cols { val := columns[i] if b, ok := val.([]byte); ok { diff --git a/internal/apps/admin/logs/routers.go b/internal/apps/admin/logs/routers.go index e8082bc3..df5209b1 100644 --- a/internal/apps/admin/logs/routers.go +++ b/internal/apps/admin/logs/routers.go @@ -56,11 +56,11 @@ func GetLogs(c *gin.Context) { limitStr := c.DefaultQuery("limit", "200") var cursor, limit int - if _, err := parsePositiveInt(cursorStr, &cursor); err != nil { + if err := parsePositiveInt(cursorStr, &cursor); err != nil { response.AbortWithError(c, http.StatusBadRequest, admin.InvalidCursorParam) return } - if _, err := parsePositiveInt(limitStr, &limit); err != nil || limit <= 0 { + if err := parsePositiveInt(limitStr, &limit); err != nil || limit <= 0 { limit = defaultLimit } if limit > maxLimit { diff --git a/internal/apps/admin/logs/utils.go b/internal/apps/admin/logs/utils.go index 3c93edcc..95acf9a8 100644 --- a/internal/apps/admin/logs/utils.go +++ b/internal/apps/admin/logs/utils.go @@ -35,8 +35,8 @@ func getUpgrader() *websocket.Upgrader { ctx := r.Context() if sc, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress); err == nil && sc.Value != "" { originToCheck := strings.TrimRight(strings.TrimSpace(origin), "/") - 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, originToCheck) { return true @@ -49,15 +49,15 @@ func getUpgrader() *websocket.Upgrader { } // parsePositiveInt 解析非负整数字符串 -func parsePositiveInt(s string, result *int) (bool, error) { +func parsePositiveInt(s string, result *int) error { if s == "" { *result = 0 - return true, nil + return nil } n, err := strconv.Atoi(s) if err != nil || n < 0 { - return false, err + return err } *result = n - return true, nil + return nil } diff --git a/internal/apps/admin/logs/utils_test.go b/internal/apps/admin/logs/utils_test.go index 2d66b6b9..daddc539 100644 --- a/internal/apps/admin/logs/utils_test.go +++ b/internal/apps/admin/logs/utils_test.go @@ -16,6 +16,7 @@ import ( ) func setupTestDB(t *testing.T) *gorm.DB { + t.Helper() dbConn, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatalf("failed to open sqlite in memory: %v", err) diff --git a/internal/apps/admin/push/channels_definition.go b/internal/apps/admin/push/channels_definition.go index 0d810569..ad5d0134 100644 --- a/internal/apps/admin/push/channels_definition.go +++ b/internal/apps/admin/push/channels_definition.go @@ -3,6 +3,8 @@ package push +import "slices" + import "sync" const ( @@ -66,13 +68,7 @@ func ListDefinitions() []Definition { } // Add any others for t, d := range definitions { - found := false - for _, o := range order { - if o == t { - found = true - break - } - } + found := slices.Contains(order, t) if !found { res = append(res, d) } diff --git a/internal/apps/admin/push/events.go b/internal/apps/admin/push/events.go index 6cc26bc2..ca5687c2 100644 --- a/internal/apps/admin/push/events.go +++ b/internal/apps/admin/push/events.go @@ -9,6 +9,7 @@ import ( "encoding/json" "errors" "fmt" + "maps" "strings" "github.com/Rain-kl/Wavelet/internal/infra/task" @@ -34,9 +35,7 @@ func (m NotificationMessage) Flatten() map[string]any { keyContent: m.Content, keyLevel: m.Level, } - for k, v := range m.Ext { - res[k] = v - } + maps.Copy(res, m.Ext) return res } diff --git a/internal/apps/admin/push/push_test.go b/internal/apps/admin/push/push_test.go index 4057e104..0a30a855 100644 --- a/internal/apps/admin/push/push_test.go +++ b/internal/apps/admin/push/push_test.go @@ -65,6 +65,7 @@ func (m *mockPusher) ValidateConfig(cfg pkgpush.Config) error { } func setupPushTest(t *testing.T) (*gorm.DB, *miniredis.Miniredis, func()) { + t.Helper() dbConn, mr, cleanup := testhelper.SetupTestEnvironment(t) // AutoMigrate push tables in SQLite test environment @@ -319,7 +320,7 @@ func TestPushHandler(t *testing.T) { assert.Equal(t, "Structured Alert", mPusher.sentBody["title"]) assert.Equal(t, "Hello World", mPusher.sentBody["content"]) assert.Equal(t, "WARNING", mPusher.sentBody["level"]) - assert.Equal(t, float64(42), mPusher.sentBody["extra_val"]) // unmarshaled json numbers are float64 by default + assert.InDelta(t, float64(42), mPusher.sentBody["extra_val"], 1e-9) // unmarshaled json numbers are float64 by default mPusher.mu.Unlock() // Verify PushHistory recorded @@ -436,7 +437,7 @@ func TestPushRouters(t *testing.T) { dataMap, ok := resp.Data.(map[string]any) assert.True(t, ok) - assert.Equal(t, float64(1), dataMap["total"]) + assert.InDelta(t, float64(1), dataMap["total"], 1e-9) }) t.Run("test push endpoint", func(t *testing.T) { @@ -614,37 +615,37 @@ func TestPushChannelAPI(t *testing.T) { t.Run("validate push channel model constraints", func(t *testing.T) { // 校验名称合法性 c1 := &model.PushChannel{Name: "invalid-name!", URL: "https://hook.com", Other: "{}"} - assert.Error(t, c1.Validate()) + require.Error(t, c1.Validate()) // 校验 URL 安全前缀 HTTPS c2 := &model.PushChannel{Name: "custom_channel", URL: "http://insecure-hook.com", Other: "{}"} - assert.Error(t, c2.Validate()) + require.Error(t, c2.Validate()) // 校验 JSON 格式 c3 := &model.PushChannel{Name: "custom_channel", URL: "https://hook.com", Other: "{invalid-json}"} - assert.Error(t, c3.Validate()) + require.Error(t, c3.Validate()) // 正确配置 c4 := &model.PushChannel{Name: "custom_channel", URL: "https://hook.com", Other: "{\"content\":\"$content\"}"} - assert.NoError(t, c4.Validate()) + require.NoError(t, c4.Validate()) // 飞书渠道校验:非 HTTPS 地址报错 c5 := &model.PushChannel{Name: "lark_channel", Type: "lark", URL: "http://open.feishu.cn", Other: ""} - assert.Error(t, c5.Validate()) + require.Error(t, c5.Validate()) // 飞书正确配置 c6 := &model.PushChannel{Name: "lark_channel", Type: "lark", URL: "https://open.feishu.cn", Other: ""} - assert.NoError(t, c6.Validate()) + require.NoError(t, c6.Validate()) // Telegram 渠道校验 cTelegramErr := &model.PushChannel{Name: "tg_channel", Type: "telegram", URL: "https://api.telegram.org", Token: "", Other: ""} - assert.Error(t, cTelegramErr.Validate()) + require.Error(t, cTelegramErr.Validate()) cTelegramErr2 := &model.PushChannel{Name: "tg_channel", Type: "telegram", URL: "http://api.telegram.org", Token: "123:abc", Other: ""} - assert.Error(t, cTelegramErr2.Validate()) + require.Error(t, cTelegramErr2.Validate()) cTelegramOk := &model.PushChannel{Name: "tg_channel", Type: "telegram", URL: "", Token: "123:abc", Other: "-100123"} - assert.NoError(t, cTelegramOk.Validate()) + require.NoError(t, cTelegramOk.Validate()) assert.Equal(t, "https://api.telegram.org", cTelegramOk.URL) // 邮件配置校验:允许空配置以复用系统全局设置 @@ -733,7 +734,7 @@ func TestPushChannelAPI(t *testing.T) { dbConn.First(&updated, createdID) assert.Equal(t, "Updated remark", updated.Description) assert.Equal(t, "new_chan_token", updated.Token) - assert.Equal(t, `{"text": "$content"}`, updated.Other) + assert.JSONEq(t, `{"text": "$content"}`, updated.Other) }) t.Run("admin test channel endpoint", func(t *testing.T) { diff --git a/internal/apps/admin/status/routers.go b/internal/apps/admin/status/routers.go index c5ad700a..37ee1434 100644 --- a/internal/apps/admin/status/routers.go +++ b/internal/apps/admin/status/routers.go @@ -14,6 +14,7 @@ import ( "os" "os/exec" "runtime" + "strconv" "time" "github.com/Rain-kl/Wavelet/internal/infra/config" @@ -309,7 +310,7 @@ func exportSQLite(c *gin.Context) { c.Header("Content-Disposition", `attachment; filename="openflare.db"`) c.Header("Content-Type", "application/octet-stream") - c.Header("Content-Length", fmt.Sprintf("%d", fi.Size())) + c.Header("Content-Length", strconv.FormatInt(fi.Size(), 10)) c.Status(http.StatusOK) http.ServeContent(c.Writer, c.Request, "openflare.db", fi.ModTime(), f) } @@ -328,7 +329,7 @@ func exportPostgres(c *gin.Context) { args := []string{ "--no-password", "-h", dbCfg.Host, - "-p", fmt.Sprintf("%d", dbCfg.Port), + "-p", strconv.Itoa(dbCfg.Port), "-U", dbCfg.Username, dbCfg.Database, } diff --git a/internal/apps/admin/task/routers_test.go b/internal/apps/admin/task/routers_test.go index 4eb14854..8ce7d0e3 100644 --- a/internal/apps/admin/task/routers_test.go +++ b/internal/apps/admin/task/routers_test.go @@ -46,6 +46,7 @@ func registerInternalOnlyTaskMeta() { } func setupTaskTestEnvironment(t *testing.T) func() { + t.Helper() _, mr, cleanup := testhelper.SetupTestEnvironment(t) bootstrap.RegisterTasks() task.AsynqClient = asynq.NewClient(asynq.RedisClientOpt{ @@ -490,7 +491,7 @@ func TestListTaskExecutions(t *testing.T) { var data map[string]interface{} json.Unmarshal(dataBytes, &data) - assert.Equal(t, float64(3), data["total"]) + assert.InDelta(t, float64(3), data["total"], 1e-9) }) t.Run("filter by status", func(t *testing.T) { @@ -507,7 +508,7 @@ func TestListTaskExecutions(t *testing.T) { var data map[string]interface{} json.Unmarshal(dataBytes, &data) - assert.Equal(t, float64(1), data["total"]) + assert.InDelta(t, float64(1), data["total"], 1e-9) }) t.Run("filter by task_type (asynq task name)", func(t *testing.T) { @@ -524,7 +525,7 @@ func TestListTaskExecutions(t *testing.T) { var data map[string]interface{} json.Unmarshal(dataBytes, &data) - assert.Equal(t, float64(3), data["total"]) + assert.InDelta(t, float64(3), data["total"], 1e-9) }) t.Run("filter by task_type (management task type)", func(t *testing.T) { @@ -541,7 +542,7 @@ func TestListTaskExecutions(t *testing.T) { var data map[string]interface{} json.Unmarshal(dataBytes, &data) - assert.Equal(t, float64(3), data["total"]) + assert.InDelta(t, float64(3), data["total"], 1e-9) }) t.Run("pagination", func(t *testing.T) { @@ -558,7 +559,7 @@ func TestListTaskExecutions(t *testing.T) { var data map[string]interface{} json.Unmarshal(dataBytes, &data) - assert.Equal(t, float64(3), data["total"]) + assert.InDelta(t, float64(3), data["total"], 1e-9) }) } diff --git a/internal/apps/admin/updater/logics.go b/internal/apps/admin/updater/logics.go index 0fe32b6a..9f1a783f 100644 --- a/internal/apps/admin/updater/logics.go +++ b/internal/apps/admin/updater/logics.go @@ -17,6 +17,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "strings" "sync" "time" @@ -163,13 +164,7 @@ func selectLatestRelease(repository string, releases []githubRelease) (githubRel } expectedNames := expectedAssetNames(repository, release.TagName) for _, asset := range release.Assets { - matched := false - for _, name := range expectedNames { - if asset.Name == name { - matched = true - break - } - } + matched := slices.Contains(expectedNames, asset.Name) if !matched || asset.BrowserDownloadURL == "" || asset.State != "uploaded" { continue } @@ -199,7 +194,7 @@ func (m *manager) fetchRelease(ctx context.Context, repository string) (githubRe } req.Header.Set("Accept", "application/vnd.github+json") req.Header.Set("User-Agent", "OpenFlare-Updater") - req.Header.Set("X-GitHub-Api-Version", "2022-11-28") + req.Header.Set("X-Github-Api-Version", "2022-11-28") resp, err := m.client.Do(req) if err != nil { @@ -348,10 +343,8 @@ func getCandidateBinaryNames(executable string, repository string) []string { if runtime.GOOS == windowsOS && !strings.HasSuffix(strings.ToLower(name), ".exe") { name += ".exe" } - for _, existing := range names { - if existing == name { - return - } + if slices.Contains(names, name) { + return } names = append(names, name) } diff --git a/internal/apps/admin/user/routers.go b/internal/apps/admin/user/routers.go index c4009424..321bb817 100644 --- a/internal/apps/admin/user/routers.go +++ b/internal/apps/admin/user/routers.go @@ -7,6 +7,7 @@ package user import ( "errors" "net/http" + "slices" "strconv" "time" @@ -93,17 +94,13 @@ func abortUserLogicError(c *gin.Context, err error, notFoundMsg string, forbidde return true } msg := err.Error() - for _, m := range badRequestMsgs { - if msg == m { - response.AbortBadRequest(c, msg) - return true - } + if slices.Contains(badRequestMsgs, msg) { + response.AbortBadRequest(c, msg) + return true } - for _, m := range forbiddenMsgs { - if msg == m { - response.AbortForbidden(c, msg) - return true - } + if slices.Contains(forbiddenMsgs, msg) { + response.AbortForbidden(c, msg) + return true } logger.ErrorF(c.Request.Context(), "Admin user error: %v", err) response.AbortInternal(c, "内部服务器错误") diff --git a/internal/apps/agent/agent/runner.go b/internal/apps/agent/agent/runner.go index 36622cd0..8ef4f3ce 100644 --- a/internal/apps/agent/agent/runner.go +++ b/internal/apps/agent/agent/runner.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package agent implements the local OpenFlare agent runtime loop. package agent diff --git a/internal/apps/agent/agent/runner_test.go b/internal/apps/agent/agent/runner_test.go index e1519599..1ec211a1 100644 --- a/internal/apps/agent/agent/runner_test.go +++ b/internal/apps/agent/agent/runner_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package agent import ( diff --git a/internal/apps/agent/config/config.go b/internal/apps/agent/config/config.go index 767bae01..06a2de7a 100644 --- a/internal/apps/agent/config/config.go +++ b/internal/apps/agent/config/config.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package config loads and persists agent daemon configuration. package config diff --git a/internal/apps/agent/config/config_test.go b/internal/apps/agent/config/config_test.go index b68e7bf6..673ad127 100644 --- a/internal/apps/agent/config/config_test.go +++ b/internal/apps/agent/config/config_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package config import ( diff --git a/internal/apps/agent/config/duration.go b/internal/apps/agent/config/duration.go index cc743d0e..83881e4c 100644 --- a/internal/apps/agent/config/duration.go +++ b/internal/apps/agent/config/duration.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package config import edgeconfig "github.com/Rain-kl/Wavelet/internal/apps/edge/config" diff --git a/internal/apps/agent/config/version.go b/internal/apps/agent/config/version.go index 9e758d4d..1c1e99c8 100644 --- a/internal/apps/agent/config/version.go +++ b/internal/apps/agent/config/version.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package config // Version is the current agent version string, overridden at build time. diff --git a/internal/apps/agent/geoipdata/data.go b/internal/apps/agent/geoipdata/data.go index e661d025..3800e81d 100644 --- a/internal/apps/agent/geoipdata/data.go +++ b/internal/apps/agent/geoipdata/data.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package geoipdata holds shared GeoIP database filename constants. // // MaxMind MMDB files are NOT embedded into the agent binary. Docker images diff --git a/internal/apps/agent/geoipupdate/updater.go b/internal/apps/agent/geoipupdate/updater.go index f6ad7745..4d32e14b 100644 --- a/internal/apps/agent/geoipupdate/updater.go +++ b/internal/apps/agent/geoipupdate/updater.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package geoipupdate schedules local MaxMind GeoIP database updates for the agent. package geoipupdate diff --git a/internal/apps/agent/geoipupdate/updater_test.go b/internal/apps/agent/geoipupdate/updater_test.go index 0fcc442a..7f74d7e7 100644 --- a/internal/apps/agent/geoipupdate/updater_test.go +++ b/internal/apps/agent/geoipupdate/updater_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package geoipupdate import ( diff --git a/internal/apps/agent/heartbeat/cycle.go b/internal/apps/agent/heartbeat/cycle.go index 4793329b..9f614eab 100644 --- a/internal/apps/agent/heartbeat/cycle.go +++ b/internal/apps/agent/heartbeat/cycle.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package heartbeat implements the periodic heartbeat cycle executed by the agent, // including payload preparation, config sync, WAF IP group application, and observability buffering. package heartbeat diff --git a/internal/apps/agent/heartbeat/service.go b/internal/apps/agent/heartbeat/service.go index bc22fdea..eae24939 100644 --- a/internal/apps/agent/heartbeat/service.go +++ b/internal/apps/agent/heartbeat/service.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package heartbeat import ( diff --git a/internal/apps/agent/logging/logger.go b/internal/apps/agent/logging/logger.go index e2900fc6..4d493850 100644 --- a/internal/apps/agent/logging/logger.go +++ b/internal/apps/agent/logging/logger.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package logging configures structured logging for the agent process. package logging diff --git a/internal/apps/agent/nginx/manager.go b/internal/apps/agent/nginx/manager.go index 8796530f..43ac6951 100644 --- a/internal/apps/agent/nginx/manager.go +++ b/internal/apps/agent/nginx/manager.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package nginx manages OpenResty configuration, runtime, and supporting assets. package nginx @@ -134,7 +137,7 @@ func (e *PathExecutor) Reload(ctx context.Context) error { slog.Warn("openresty reload reported runtime is not running, starting binary", "path", e.Path) startOutput, startErr := e.Runner.Run(ctx, e.Path, "-c", e.ConfigPath) if startErr != nil { - return fmt.Errorf("openresty reload failed: %w: %s; start failed: %v: %s", err, string(output), startErr, string(startOutput)) + return fmt.Errorf("openresty reload failed: %w: %s; start failed: %w: %s", err, string(output), startErr, string(startOutput)) } return nil } @@ -355,14 +358,14 @@ func (m *Manager) activateConfig(ctx context.Context) error { func (m *Manager) rollbackAfterFailedApply(ctx context.Context, backup *backupState, applyErr error) ApplyOutcome { slog.Warn("openresty apply failed, restoring previous config", "error", applyErr) if err := m.restore(backup); err != nil { - return fatalApplyOutcome(fmt.Errorf("restore openresty backup failed after apply error %v: %w", applyErr, err)) + return fatalApplyOutcome(fmt.Errorf("restore openresty backup failed after apply error %w: %w", applyErr, err)) } if err := m.activateConfig(ctx); err != nil { if backup != nil && backup.MainExisted { - return fatalApplyOutcome(fmt.Errorf("apply failed: %v; rollback recovery failed: %w", applyErr, err)) + return fatalApplyOutcome(fmt.Errorf("apply failed: %w; rollback recovery failed: %w", applyErr, err)) } if fallbackErr := m.EnsureSafeFallbackRuntime(ctx, fmt.Sprintf("apply failed: %v; rollback recovery failed: %v", applyErr, err)); fallbackErr != nil { - return fatalApplyOutcome(fmt.Errorf("apply failed: %v; rollback recovery failed: %w; fallback recovery failed: %v", applyErr, err, fallbackErr)) + return fatalApplyOutcome(fmt.Errorf("apply failed: %w; rollback recovery failed: %w; fallback recovery failed: %w", applyErr, err, fallbackErr)) } message := fmt.Sprintf("apply failed, but fallback runtime started: %v; rollback recovery failed: %v", applyErr, err) slog.Warn("openresty apply recovered with safe default fallback", "message", message) @@ -516,7 +519,7 @@ func (m *Manager) CurrentChecksum() (string, error) { normalizedMain = strings.ReplaceAll(normalizedMain, listen, openrestyrender.ObservabilityListenPlaceholder) } if m.OpenrestyObservabilityPort > 0 { - normalizedMain = strings.ReplaceAll(normalizedMain, fmt.Sprintf("%d", m.OpenrestyObservabilityPort), openrestyrender.ObservabilityPortPlaceholder) + normalizedMain = strings.ReplaceAll(normalizedMain, strconv.Itoa(m.OpenrestyObservabilityPort), openrestyrender.ObservabilityPortPlaceholder) } if resolverDirective := strings.TrimSpace(m.OpenrestyResolverDirective); resolverDirective != "" { normalizedMain = strings.ReplaceAll(normalizedMain, resolverDirective, ResolverDirectivePlaceholder) @@ -1463,7 +1466,7 @@ func (m *Manager) renderMainConfig(content string) string { rendered = strings.ReplaceAll(rendered, openrestyrender.ObservabilityListenPlaceholder, listen) } if m.OpenrestyObservabilityPort > 0 { - rendered = strings.ReplaceAll(rendered, openrestyrender.ObservabilityPortPlaceholder, fmt.Sprintf("%d", m.OpenrestyObservabilityPort)) + rendered = strings.ReplaceAll(rendered, openrestyrender.ObservabilityPortPlaceholder, strconv.Itoa(m.OpenrestyObservabilityPort)) } if resolverDirective := strings.TrimSpace(m.OpenrestyResolverDirective); resolverDirective != "" { rendered = strings.ReplaceAll(rendered, ResolverDirectivePlaceholder, resolverDirective) diff --git a/internal/apps/agent/nginx/manager_test.go b/internal/apps/agent/nginx/manager_test.go index e9789540..a55d3061 100644 --- a/internal/apps/agent/nginx/manager_test.go +++ b/internal/apps/agent/nginx/manager_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package nginx import ( diff --git a/internal/apps/agent/nginx/mimetypes.go b/internal/apps/agent/nginx/mimetypes.go index 8d2fb044..cb6f6a03 100644 --- a/internal/apps/agent/nginx/mimetypes.go +++ b/internal/apps/agent/nginx/mimetypes.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package nginx // DefaultMimeTypes is the embedded nginx mime.types map used by generated configs. diff --git a/internal/apps/agent/nginx/observability_assets.go b/internal/apps/agent/nginx/observability_assets.go index 55b05181..8c0f4a49 100644 --- a/internal/apps/agent/nginx/observability_assets.go +++ b/internal/apps/agent/nginx/observability_assets.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package nginx import "github.com/Rain-kl/Wavelet/internal/apps/agent/protocol" diff --git a/internal/apps/agent/nginx/observability_assets_test.go b/internal/apps/agent/nginx/observability_assets_test.go index c34891a8..b74646d9 100644 --- a/internal/apps/agent/nginx/observability_assets_test.go +++ b/internal/apps/agent/nginx/observability_assets_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package nginx import ( diff --git a/internal/apps/agent/nginx/pow_assets.go b/internal/apps/agent/nginx/pow_assets.go index a1fe5aa9..39571142 100644 --- a/internal/apps/agent/nginx/pow_assets.go +++ b/internal/apps/agent/nginx/pow_assets.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package nginx import ( diff --git a/internal/apps/agent/nginx/pow_assets_test.go b/internal/apps/agent/nginx/pow_assets_test.go index 5ba80af8..bf457808 100644 --- a/internal/apps/agent/nginx/pow_assets_test.go +++ b/internal/apps/agent/nginx/pow_assets_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package nginx import ( diff --git a/internal/apps/agent/nginx/runtime_access.go b/internal/apps/agent/nginx/runtime_access.go index 7b733ca2..320e734e 100644 --- a/internal/apps/agent/nginx/runtime_access.go +++ b/internal/apps/agent/nginx/runtime_access.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package nginx import ( diff --git a/internal/apps/agent/nginx/runtime_access_test.go b/internal/apps/agent/nginx/runtime_access_test.go index e8678f8a..7df4139c 100644 --- a/internal/apps/agent/nginx/runtime_access_test.go +++ b/internal/apps/agent/nginx/runtime_access_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package nginx import ( diff --git a/internal/apps/agent/nginx/sw_assets.go b/internal/apps/agent/nginx/sw_assets.go index 135b958e..4462fc49 100644 --- a/internal/apps/agent/nginx/sw_assets.go +++ b/internal/apps/agent/nginx/sw_assets.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package nginx import ( diff --git a/internal/apps/agent/nginx/sw_assets_test.go b/internal/apps/agent/nginx/sw_assets_test.go index d59e5804..cf3b4020 100644 --- a/internal/apps/agent/nginx/sw_assets_test.go +++ b/internal/apps/agent/nginx/sw_assets_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package nginx import ( diff --git a/internal/apps/agent/nginx/waf_assets.go b/internal/apps/agent/nginx/waf_assets.go index 1ecd0ef2..ba41afb5 100644 --- a/internal/apps/agent/nginx/waf_assets.go +++ b/internal/apps/agent/nginx/waf_assets.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package nginx import ( diff --git a/internal/apps/agent/nginx/waf_assets_test.go b/internal/apps/agent/nginx/waf_assets_test.go index 289b119c..2459ea80 100644 --- a/internal/apps/agent/nginx/waf_assets_test.go +++ b/internal/apps/agent/nginx/waf_assets_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package nginx import ( diff --git a/internal/apps/agent/observability/collector.go b/internal/apps/agent/observability/collector.go index 1bf1e4fd..ce991aba 100644 --- a/internal/apps/agent/observability/collector.go +++ b/internal/apps/agent/observability/collector.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package observability provides system and service level observability data collection for the agent. package observability diff --git a/internal/apps/agent/observability/openresty_local.go b/internal/apps/agent/observability/openresty_local.go index e9aeb0fd..d77355e2 100644 --- a/internal/apps/agent/observability/openresty_local.go +++ b/internal/apps/agent/observability/openresty_local.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package observability import ( @@ -62,7 +65,7 @@ func CollectEdgeHealth(ctx context.Context, cfg *config.Config) *EdgeHealthSnaps } func fetchLocalJSON(ctx context.Context, client *http.Client, url string, target any) error { - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err } diff --git a/internal/apps/agent/observability/openresty_local_test.go b/internal/apps/agent/observability/openresty_local_test.go index 37e41458..20fbae76 100644 --- a/internal/apps/agent/observability/openresty_local_test.go +++ b/internal/apps/agent/observability/openresty_local_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package observability import ( diff --git a/internal/apps/agent/observability/traffic.go b/internal/apps/agent/observability/traffic.go index 193d4f08..5bd22853 100644 --- a/internal/apps/agent/observability/traffic.go +++ b/internal/apps/agent/observability/traffic.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package observability import ( diff --git a/internal/apps/agent/observability/traffic_test.go b/internal/apps/agent/observability/traffic_test.go index 451b7bb3..dd8d658b 100644 --- a/internal/apps/agent/observability/traffic_test.go +++ b/internal/apps/agent/observability/traffic_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package observability import ( diff --git a/internal/apps/agent/protocol/alias.go b/internal/apps/agent/protocol/alias.go index a7c5ce0c..653670b2 100644 --- a/internal/apps/agent/protocol/alias.go +++ b/internal/apps/agent/protocol/alias.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package protocol defines type aliases and constants for the agent protocol. package protocol diff --git a/internal/apps/agent/runtimeuser/runtimeuser.go b/internal/apps/agent/runtimeuser/runtimeuser.go index bf8e43a0..691f8030 100644 --- a/internal/apps/agent/runtimeuser/runtimeuser.go +++ b/internal/apps/agent/runtimeuser/runtimeuser.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package runtimeuser defines the shared OS account used by the agent process // and OpenResty worker processes so file ownership stays aligned. package runtimeuser @@ -120,7 +123,7 @@ func ensureWorldTraversablePath(targetDir string) error { if current == "" || current == "." { return nil } - for depth := 0; depth < maxDepth; depth++ { + for range maxDepth { if err := os.Chmod(current, DefaultDirPerm); err != nil { //nolint:gosec // parent dirs must be traversable by the runtime user if os.IsNotExist(err) || os.IsPermission(err) { break diff --git a/internal/apps/agent/runtimeuser/runtimeuser_test.go b/internal/apps/agent/runtimeuser/runtimeuser_test.go index 8a392e97..46d1fe6a 100644 --- a/internal/apps/agent/runtimeuser/runtimeuser_test.go +++ b/internal/apps/agent/runtimeuser/runtimeuser_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package runtimeuser import ( diff --git a/internal/apps/agent/runtimeuser/runtimeuser_unix.go b/internal/apps/agent/runtimeuser/runtimeuser_unix.go index b522d74d..41f33b6a 100644 --- a/internal/apps/agent/runtimeuser/runtimeuser_unix.go +++ b/internal/apps/agent/runtimeuser/runtimeuser_unix.go @@ -1,5 +1,8 @@ //go:build unix +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package runtimeuser import ( diff --git a/internal/apps/agent/state/observability_buffer.go b/internal/apps/agent/state/observability_buffer.go index 2509e183..a369f9a0 100644 --- a/internal/apps/agent/state/observability_buffer.go +++ b/internal/apps/agent/state/observability_buffer.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package state persists agent runtime state and observability snapshots. package state diff --git a/internal/apps/agent/state/observability_buffer_test.go b/internal/apps/agent/state/observability_buffer_test.go index 75e5b2ae..19fb527c 100644 --- a/internal/apps/agent/state/observability_buffer_test.go +++ b/internal/apps/agent/state/observability_buffer_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package state import ( diff --git a/internal/apps/agent/state/state.go b/internal/apps/agent/state/state.go index e242cc31..e57b45b6 100644 --- a/internal/apps/agent/state/state.go +++ b/internal/apps/agent/state/state.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package state import ( diff --git a/internal/apps/agent/state/state_test.go b/internal/apps/agent/state/state_test.go index 48719655..b34a067c 100644 --- a/internal/apps/agent/state/state_test.go +++ b/internal/apps/agent/state/state_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package state import ( diff --git a/internal/apps/agent/sync/pages.go b/internal/apps/agent/sync/pages.go index 5364ec4d..840eee8c 100644 --- a/internal/apps/agent/sync/pages.go +++ b/internal/apps/agent/sync/pages.go @@ -15,6 +15,7 @@ import ( "log/slog" "os" "path/filepath" + "strconv" "strings" "github.com/Rain-kl/Wavelet/internal/apps/agent/protocol" @@ -198,7 +199,7 @@ func (s *Service) ensurePagesProject(ctx context.Context, snapshot *state.Snapsh } var lastErr error - for attempt := 0; attempt < pagesLatestPullAttempts; attempt++ { + for attempt := range pagesLatestPullAttempts { latest, err := s.client.GetPagesProjectLatestHash(ctx, projectID) if err != nil { return fmt.Errorf("fetch Pages project %d latest hash: %w", projectID, err) @@ -359,10 +360,7 @@ func validatePagesPackageMetadata( if extractedBytes == 0 { extractedBytes = 1 } - maxFileBytes := extractedBytes - if maxFileBytes > agentPagesMaxFileBytes { - maxFileBytes = agentPagesMaxFileBytes - } + maxFileBytes := min(extractedBytes, agentPagesMaxFileBytes) return pagesPackageLimits{ PackageBytes: metadata.PackageSize, @@ -395,7 +393,7 @@ func (s *Service) downloadPagesProjectPackage( metadata *protocol.PagesProjectLatestHashResponse, maxBytes int64, ) (packagePath string, hash string, err error) { - releasesRoot := filepath.Join(s.pagesDir, "projects", fmt.Sprintf("%d", projectID), "releases") + releasesRoot := filepath.Join(s.pagesDir, "projects", strconv.FormatUint(uint64(projectID), 10), "releases") if err := os.MkdirAll(releasesRoot, pagesDirPerm); err != nil { return "", "", err } @@ -444,7 +442,7 @@ func cleanupPagesProjectStaleReleases(baseDir string, projectID uint, keepHash s if projectID == 0 || keepHash == "" { return nil } - releasesRoot := filepath.Join(baseDir, "projects", fmt.Sprintf("%d", projectID), "releases") + releasesRoot := filepath.Join(baseDir, "projects", strconv.FormatUint(uint64(projectID), 10), "releases") entries, err := os.ReadDir(releasesRoot) //nolint:gosec // managed PagesDir if err != nil { if os.IsNotExist(err) { @@ -985,9 +983,9 @@ func writePagesMarker(dir string, project pagesProjectRef) error { } func pagesProjectCurrentDir(baseDir string, projectID uint) string { - return filepath.Join(baseDir, "projects", fmt.Sprintf("%d", projectID), "current") + return filepath.Join(baseDir, "projects", strconv.FormatUint(uint64(projectID), 10), "current") } func pagesProjectReleaseDir(baseDir string, projectID uint, checksum string) string { - return filepath.Join(baseDir, "projects", fmt.Sprintf("%d", projectID), "releases", checksum) + return filepath.Join(baseDir, "projects", strconv.FormatUint(uint64(projectID), 10), "releases", checksum) } diff --git a/internal/apps/agent/sync/pages_stream_test.go b/internal/apps/agent/sync/pages_stream_test.go index 8c16e50d..5db5ae91 100644 --- a/internal/apps/agent/sync/pages_stream_test.go +++ b/internal/apps/agent/sync/pages_stream_test.go @@ -238,10 +238,7 @@ func TestPromoteSameHashReleaseFailureRestoresPreviousCurrent(t *testing.T) { if err := switchPagesProjectCurrentDir(pagesDir, projectID, releaseDir); err != nil { t.Fatalf("seed same-hash current error = %v", err) } - stagingDir, err := os.MkdirTemp(filepath.Dir(releaseDir), ".same-hash-*.tmp") - if err != nil { - t.Fatalf("create same-hash staging error = %v", err) - } + stagingDir := t.TempDir() if err := os.WriteFile(filepath.Join(stagingDir, "index.html"), []byte("new"), pagesFilePerm); err != nil { t.Fatalf("write repaired same-hash release error = %v", err) } @@ -249,7 +246,7 @@ func TestPromoteSameHashReleaseFailureRestoresPreviousCurrent(t *testing.T) { t.Fatalf("write repaired same-hash marker error = %v", err) } copyErr := errors.New("injected same-hash copy failure") - err = promotePagesReleaseWithCopy( + err := promotePagesReleaseWithCopy( stagingDir, releaseDir, project, @@ -298,10 +295,7 @@ func TestPromotePagesReleaseRepairsDanglingCurrent(t *testing.T) { } requireTestMkdirAll(t, filepath.Dir(releaseDir)) - stagingDir, err := os.MkdirTemp(filepath.Dir(releaseDir), ".dangling-*.tmp") - if err != nil { - t.Fatalf("create dangling repair staging error = %v", err) - } + stagingDir := t.TempDir() if err := os.WriteFile(filepath.Join(stagingDir, "index.html"), []byte("repaired"), pagesFilePerm); err != nil { t.Fatalf("write dangling repair staging error = %v", err) } diff --git a/internal/apps/agent/sync/service.go b/internal/apps/agent/sync/service.go index 8798d35e..ee5d4ce1 100644 --- a/internal/apps/agent/sync/service.go +++ b/internal/apps/agent/sync/service.go @@ -12,7 +12,7 @@ import ( "fmt" "io" "log/slog" - "sort" + "slices" "strconv" "strings" "sync" @@ -363,7 +363,7 @@ func referencedWAFIPGroupIDs(supportFiles []protocol.SupportFile) ([]uint, error for id := range seen { ids = append(ids, id) } - sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) + slices.Sort(ids) return ids, nil } @@ -426,7 +426,7 @@ func (s *Service) ensureRuntimeForCurrentConfig(ctx context.Context, mode string snapshot.OpenrestyMessage = "safe default fallback runtime started" return nil } - err = fmt.Errorf("%v; fallback recovery failed: %w", err, fallbackErr) + err = fmt.Errorf("%w; fallback recovery failed: %w", err, fallbackErr) } snapshot.OpenrestyStatus = protocol.OpenrestyStatusUnhealthy snapshot.OpenrestyMessage = err.Error() diff --git a/internal/apps/agent/sync/sync_helpers.go b/internal/apps/agent/sync/sync_helpers.go index e3269d36..e4973267 100644 --- a/internal/apps/agent/sync/sync_helpers.go +++ b/internal/apps/agent/sync/sync_helpers.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package sync import ( @@ -220,6 +223,9 @@ func updateSnapshotFromApplyOutcome(mode string, snapshot *state.Snapshot, confi snapshot.OpenrestyStatus = protocol.OpenrestyStatusHealthy snapshot.OpenrestyMessage = result.message result.reportResult = ApplyResultWarning + case nginx.ApplyStatusFatal: + // 致命错误与普通失败同走失败路径:标记阻塞并上报 Unhealthy。 + fallthrough default: if result.message == "" { result.message = "openresty apply failed" diff --git a/internal/apps/agent/updater/updater.go b/internal/apps/agent/updater/updater.go index ce8e16e3..e8749d32 100644 --- a/internal/apps/agent/updater/updater.go +++ b/internal/apps/agent/updater/updater.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package updater provides agent self-update integration with the edge updater. package updater diff --git a/internal/apps/agent/wsclient/client.go b/internal/apps/agent/wsclient/client.go index 7530c033..f25e2149 100644 --- a/internal/apps/agent/wsclient/client.go +++ b/internal/apps/agent/wsclient/client.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package wsclient provides the agent-side WebSocket client for connecting to the OpenFlare server. package wsclient diff --git a/internal/apps/cap/manager.go b/internal/apps/cap/manager.go index b99fff24..474993c1 100644 --- a/internal/apps/cap/manager.go +++ b/internal/apps/cap/manager.go @@ -78,10 +78,7 @@ func (m *Manager) Redeem(ctx context.Context, token string, solutions []int, sco } now := time.Now().UnixNano() / int64(time.Millisecond) - nonceTTL := time.Duration(payload.Expires-now) * time.Millisecond - if nonceTTL < time.Second { - nonceTTL = time.Second - } + nonceTTL := max(time.Duration(payload.Expires-now)*time.Millisecond, time.Second) set, err := m.store.SetNX(ctx, nonceKey, "1", nonceTTL) if err != nil { diff --git a/internal/apps/edge/config/duration.go b/internal/apps/edge/config/duration.go index 86303c08..6665efae 100644 --- a/internal/apps/edge/config/duration.go +++ b/internal/apps/edge/config/duration.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package config provides shared configuration types for edge applications. package config diff --git a/internal/apps/edge/config/duration_test.go b/internal/apps/edge/config/duration_test.go index 00362a34..ecdb8a3a 100644 --- a/internal/apps/edge/config/duration_test.go +++ b/internal/apps/edge/config/duration_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package config import ( diff --git a/internal/apps/edge/heartbeat/autoupdate.go b/internal/apps/edge/heartbeat/autoupdate.go index bda6cab2..09cd69c0 100644 --- a/internal/apps/edge/heartbeat/autoupdate.go +++ b/internal/apps/edge/heartbeat/autoupdate.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package heartbeat handles periodic heartbeat and update checks. package heartbeat diff --git a/internal/apps/edge/heartbeat/loop.go b/internal/apps/edge/heartbeat/loop.go index a79da752..7b8523ef 100644 --- a/internal/apps/edge/heartbeat/loop.go +++ b/internal/apps/edge/heartbeat/loop.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package heartbeat import ( diff --git a/internal/apps/edge/heartbeat/loop_test.go b/internal/apps/edge/heartbeat/loop_test.go index 6d9df1c5..76f0884b 100644 --- a/internal/apps/edge/heartbeat/loop_test.go +++ b/internal/apps/edge/heartbeat/loop_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package heartbeat import ( diff --git a/internal/apps/edge/httpclient/client.go b/internal/apps/edge/httpclient/client.go index 3f4b5420..77ae3ef6 100644 --- a/internal/apps/edge/httpclient/client.go +++ b/internal/apps/edge/httpclient/client.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package httpclient provides an authenticated HTTP client for edge services. package httpclient diff --git a/internal/apps/edge/logging/logging.go b/internal/apps/edge/logging/logging.go index b80071b5..2763f0f3 100644 --- a/internal/apps/edge/logging/logging.go +++ b/internal/apps/edge/logging/logging.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package logging configures structured logging for edge applications. package logging diff --git a/internal/apps/edge/nodeip/nodeip.go b/internal/apps/edge/nodeip/nodeip.go index 61233a93..2a3a5e96 100644 --- a/internal/apps/edge/nodeip/nodeip.go +++ b/internal/apps/edge/nodeip/nodeip.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package nodeip detects the preferred public IP address for edge nodes. package nodeip diff --git a/internal/apps/edge/observability/linux.go b/internal/apps/edge/observability/linux.go index e7796c79..2c5f7c2b 100644 --- a/internal/apps/edge/observability/linux.go +++ b/internal/apps/edge/observability/linux.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package observability provides helpers that read Linux /proc and /sys metrics for system monitoring. package observability @@ -96,10 +99,7 @@ func ReadMemInfo() (int64, int64) { if total == 0 { return 0, 0 } - used := total - (memAvailableKB * 1024) - if used < 0 { - used = 0 - } + used := max(total-(memAvailableKB*1024), 0) return total, used } @@ -138,8 +138,8 @@ func ReadLinuxCPUStat() (uint64, uint64) { if err != nil { return 0, 0 } - lines := strings.Split(string(content), "\n") - for _, line := range lines { + lines := strings.SplitSeq(string(content), "\n") + for line := range lines { if !strings.HasPrefix(line, "cpu ") { continue } @@ -258,23 +258,23 @@ func StatFilesystem(path string) (int64, int64) { if err := syscall.Statfs(absPath, &stat); err != nil { return 0, 0 } - total := multiplyUint64ToInt64(stat.Blocks, uint64(stat.Bsize)) - free := multiplyUint64ToInt64(stat.Bavail, uint64(stat.Bsize)) - used := total - free - if used < 0 { - used = 0 - } + total := multiplyUint64Int64(stat.Blocks, stat.Bsize) + free := multiplyUint64Int64(stat.Bavail, stat.Bsize) + used := max(total-free, 0) return total, used } -func multiplyUint64ToInt64(a uint64, b uint64) int64 { - if a == 0 || b == 0 { +// multiplyUint64Int64 multiplies a uint64 by a positive int64, saturating at +// math.MaxInt64 to avoid int64 overflow. +func multiplyUint64Int64(a uint64, b int64) int64 { + if a == 0 || b <= 0 { return 0 } - if a > math.MaxInt64/b { + v := a * uint64(b) + if v > math.MaxInt64 { return math.MaxInt64 } - return int64(a * b) //nolint:gosec // product is bounded to math.MaxInt64 above + return int64(v) } // ReadFirstLine reads and returns the trimmed first line of a file. diff --git a/internal/apps/edge/observability/linux_test.go b/internal/apps/edge/observability/linux_test.go index 7a28fbd2..8acee91e 100644 --- a/internal/apps/edge/observability/linux_test.go +++ b/internal/apps/edge/observability/linux_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package observability import ( diff --git a/internal/apps/edge/runner/wsreconnect.go b/internal/apps/edge/runner/wsreconnect.go index 4db235d0..f56c2785 100644 --- a/internal/apps/edge/runner/wsreconnect.go +++ b/internal/apps/edge/runner/wsreconnect.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package runner provides shared WebSocket reconnect helpers for edge daemons. package runner diff --git a/internal/apps/edge/updater/restart_unix.go b/internal/apps/edge/updater/restart_unix.go index 403aac11..c350eecf 100644 --- a/internal/apps/edge/updater/restart_unix.go +++ b/internal/apps/edge/updater/restart_unix.go @@ -1,9 +1,13 @@ //go:build !windows +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package updater provides capabilities to check for, download, and apply updates. package updater import ( + "errors" "fmt" "log/slog" "os" @@ -19,7 +23,7 @@ func replaceAndRestart(execPath string, tmpPath string) error { renameErr := err if err := os.Remove(tmpPath); err != nil && !os.IsNotExist(err) { slog.Error("remove tmp binary failed", "path", tmpPath, "error", err) - return fmt.Errorf("backup current binary: %w; remove tmp binary: %v", renameErr, err) + return fmt.Errorf("backup current binary: %w; remove tmp binary: %w", renameErr, err) } return fmt.Errorf("backup current binary: %w", renameErr) } @@ -27,7 +31,7 @@ func replaceAndRestart(execPath string, tmpPath string) error { replaceErr := err if err := os.Rename(backupPath, execPath); err != nil { slog.Error("restore backup binary failed", "path", backupPath, "error", err) - return fmt.Errorf("replace binary: %w; restore backup binary: %v", replaceErr, err) + return fmt.Errorf("replace binary: %w; restore backup binary: %w", replaceErr, err) } return fmt.Errorf("replace binary: %w", replaceErr) } @@ -37,7 +41,7 @@ func replaceAndRestart(execPath string, tmpPath string) error { if err := syscall.Exec(execPath, os.Args, os.Environ()); err != nil { //nolint:gosec // execPath is the validated edge updater binary path return fmt.Errorf("exec restart: %w", err) } - return fmt.Errorf("unreachable after exec") + return errors.New("unreachable after exec") } func removeBackupBinary(path string) error { diff --git a/internal/apps/edge/updater/restart_unix_test.go b/internal/apps/edge/updater/restart_unix_test.go index a984346a..581da678 100644 --- a/internal/apps/edge/updater/restart_unix_test.go +++ b/internal/apps/edge/updater/restart_unix_test.go @@ -1,5 +1,8 @@ //go:build !windows +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package updater import ( diff --git a/internal/apps/edge/updater/restart_windows.go b/internal/apps/edge/updater/restart_windows.go index 66cb5e61..2e2d7da3 100644 --- a/internal/apps/edge/updater/restart_windows.go +++ b/internal/apps/edge/updater/restart_windows.go @@ -1,5 +1,8 @@ //go:build windows +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package updater import ( diff --git a/internal/apps/edge/updater/service.go b/internal/apps/edge/updater/service.go index 86833883..26ed56a9 100644 --- a/internal/apps/edge/updater/service.go +++ b/internal/apps/edge/updater/service.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package updater provides capabilities to check for, download, and apply updates. package updater @@ -6,6 +9,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "log/slog" @@ -285,7 +289,7 @@ func (s *Service) downloadChecksum(ctx context.Context, url string, assetName st func parseSHA256Checksum(content string, assetName string) (string, error) { assetName = strings.TrimSpace(assetName) - for _, line := range strings.Split(content, "\n") { + for line := range strings.SplitSeq(content, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue @@ -295,7 +299,7 @@ func parseSHA256Checksum(content string, assetName string) (string, error) { } } if assetName == "" { - return "", fmt.Errorf("checksum asset does not contain a valid sha256 digest") + return "", errors.New("checksum asset does not contain a valid sha256 digest") } return "", fmt.Errorf("checksum asset does not contain a sha256 digest for %q", assetName) } @@ -340,7 +344,7 @@ func isSHA256Hex(value string) bool { func (s *Service) downloadAndRestart(ctx context.Context, url string, expectedChecksum string, targetPath string) error { expectedChecksum = strings.ToLower(strings.TrimSpace(expectedChecksum)) if !isSHA256Hex(expectedChecksum) { - return fmt.Errorf("invalid expected sha256 checksum") + return errors.New("invalid expected sha256 checksum") } req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { diff --git a/internal/apps/edge/updater/service_test.go b/internal/apps/edge/updater/service_test.go index e50bbc33..1367d8fa 100644 --- a/internal/apps/edge/updater/service_test.go +++ b/internal/apps/edge/updater/service_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package updater import ( diff --git a/internal/apps/edge/wsclient/client.go b/internal/apps/edge/wsclient/client.go index fbb006eb..ce3e1de9 100644 --- a/internal/apps/edge/wsclient/client.go +++ b/internal/apps/edge/wsclient/client.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package wsclient provides WebSocket client abstractions for edge node communication. package wsclient diff --git a/internal/apps/edge/wsclient/client_test.go b/internal/apps/edge/wsclient/client_test.go index 591ee9d5..a4e5961a 100644 --- a/internal/apps/edge/wsclient/client_test.go +++ b/internal/apps/edge/wsclient/client_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package wsclient import ( diff --git a/internal/apps/flared/config/config.go b/internal/apps/flared/config/config.go index 11038304..0e61a88a 100644 --- a/internal/apps/flared/config/config.go +++ b/internal/apps/flared/config/config.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package config loads and persists flared daemon configuration. package config diff --git a/internal/apps/flared/config/version.go b/internal/apps/flared/config/version.go index 657a95de..e6dd9ee6 100644 --- a/internal/apps/flared/config/version.go +++ b/internal/apps/flared/config/version.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package config // Version is the flared daemon build version string. diff --git a/internal/apps/flared/flared/runner.go b/internal/apps/flared/flared/runner.go index 87743a54..5253964d 100644 --- a/internal/apps/flared/flared/runner.go +++ b/internal/apps/flared/flared/runner.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package flared implements the tunnel client daemon runtime loop. package flared diff --git a/internal/apps/flared/frpc/manager.go b/internal/apps/flared/frpc/manager.go index 2b0c9598..2b3b9f43 100644 --- a/internal/apps/flared/frpc/manager.go +++ b/internal/apps/flared/frpc/manager.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package frpc manages frpc child processes for tunnel relay connections. package frpc @@ -195,6 +198,17 @@ func (m *Manager) restartProcess(ctx context.Context, relayID string, configPath var stderrBuf bytes.Buffer cmd.Stderr = &stderrBuf + // frpc 及其中间子进程必须整体随上下文终止:CommandContext 默认只杀 + // 直接子进程,孤儿孙进程会继续持有 stderr 管道导致 cmd.Wait 阻塞到其 + // 自然退出。这里为 frpc 单独建进程组并整组 SIGKILL。 + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + cmd.Cancel = func() error { + if cmd.Process == nil { + return nil + } + return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) + } + m.mu.Lock() proc.Cmd = cmd proc.Status = "running" @@ -203,7 +217,7 @@ func (m *Manager) restartProcess(ctx context.Context, relayID string, configPath startedAt := time.Now() err := cmd.Start() if err == nil { - _ = os.WriteFile(pidPath, []byte(fmt.Sprintf("%d", cmd.Process.Pid)), frpcConfigFilePerm) + _ = os.WriteFile(pidPath, fmt.Appendf(nil, "%d", cmd.Process.Pid), frpcConfigFilePerm) err = cmd.Wait() } _ = os.Remove(pidPath) diff --git a/internal/apps/flared/frpc/manager_test.go b/internal/apps/flared/frpc/manager_test.go index e2b3b658..88e6af4e 100644 --- a/internal/apps/flared/frpc/manager_test.go +++ b/internal/apps/flared/frpc/manager_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + package frpc import ( @@ -7,6 +10,7 @@ import ( "os/exec" "path/filepath" "strings" + "syscall" "testing" "time" @@ -16,6 +20,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 +31,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_frpc") @@ -53,12 +59,17 @@ exit "${EXIT_CODE:-0}" // Helper to poll for status to eliminate timing flakiness in tests func assertStatusEventually(t *testing.T, m *Manager, relayID string, expectedStatus string, timeout time.Duration) { + t.Helper() deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { m.mu.RLock() proc, ok := m.processes[relayID] + var status string + if ok { + status = proc.Status + } m.mu.RUnlock() - if ok && proc.Status == expectedStatus { + if ok && status == expectedStatus { return } time.Sleep(50 * time.Millisecond) @@ -77,6 +88,7 @@ func assertStatusEventually(t *testing.T, m *Manager, relayID string, expectedSt t.Fatalf("expected status eventually %s, got %s (err: %s)", expectedStatus, got, errStr) } +// assertCommandExitedEventually 等待测试自建进程退出(本测试持有其 Wait 权)。 func assertCommandExitedEventually(t *testing.T, cmd *exec.Cmd, timeout time.Duration) { t.Helper() @@ -92,6 +104,22 @@ func assertCommandExitedEventually(t *testing.T, cmd *exec.Cmd, timeout time.Dur } } +// assertManagedCommandExitedEventually 探测受管进程是否已退出。不能对其调用 +// Wait —— Wait 由 Manager 拥有,测试并发 Wait 会与 os/exec 的 ctxResult +// 通道竞争而永久挂起;Signal(0) 在进程被 Manager 收割后即报错。 +func assertManagedCommandExitedEventually(t *testing.T, cmd *exec.Cmd, timeout time.Duration) { + t.Helper() + + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if err := cmd.Process.Signal(syscall.Signal(0)); err != nil { + return + } + time.Sleep(50 * time.Millisecond) + } + t.Fatalf("expected managed process pid=%d to exit within %s", cmd.Process.Pid, timeout) +} + func TestStartProcessSuccess(t *testing.T) { scriptPath, dir := setupDummyScript(t) writeControl(t, dir, 0, 5) // exit code 0, sleep 5s @@ -377,7 +405,7 @@ func TestStopCancelsRunningProcesses(t *testing.T) { m.mu.RUnlock() m.Stop() - assertCommandExitedEventually(t, cmd, 2*time.Second) + assertManagedCommandExitedEventually(t, cmd, 2*time.Second) m.mu.RLock() defer m.mu.RUnlock() diff --git a/internal/apps/flared/heartbeat/service.go b/internal/apps/flared/heartbeat/service.go index a9654720..3441b14d 100644 --- a/internal/apps/flared/heartbeat/service.go +++ b/internal/apps/flared/heartbeat/service.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package heartbeat runs the periodic flared heartbeat loop against the control plane. package heartbeat diff --git a/internal/apps/flared/httpclient/client.go b/internal/apps/flared/httpclient/client.go index 2426f21e..b46e5ce4 100644 --- a/internal/apps/flared/httpclient/client.go +++ b/internal/apps/flared/httpclient/client.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package httpclient provides the HTTP client used by the flared agent to communicate with the Wavelet server. package httpclient diff --git a/internal/apps/flared/sync/service.go b/internal/apps/flared/sync/service.go index 35cc290e..0bc86e82 100644 --- a/internal/apps/flared/sync/service.go +++ b/internal/apps/flared/sync/service.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package sync periodically fetches and applies the active tunnel configuration. package sync diff --git a/internal/apps/flared/updater/updater.go b/internal/apps/flared/updater/updater.go index 7fbeb2fd..fa9ad6de 100644 --- a/internal/apps/flared/updater/updater.go +++ b/internal/apps/flared/updater/updater.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package updater provides update service capabilities for flared. package updater diff --git a/internal/apps/flared/wsclient/client.go b/internal/apps/flared/wsclient/client.go index 389a0adb..9fef647f 100644 --- a/internal/apps/flared/wsclient/client.go +++ b/internal/apps/flared/wsclient/client.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package wsclient provides a WebSocket client for flared control-plane communication. package wsclient diff --git a/internal/apps/oauth/auth_source_resolver.go b/internal/apps/oauth/auth_source_resolver.go index e996c4be..306ad5c2 100644 --- a/internal/apps/oauth/auth_source_resolver.go +++ b/internal/apps/oauth/auth_source_resolver.go @@ -6,6 +6,7 @@ package oauth import ( "context" "errors" + "slices" "strings" "github.com/Rain-kl/Wavelet/internal/model" @@ -109,10 +110,5 @@ func buildOAuthConfig(ctx context.Context, source *model.AuthSource, redirectURL } func containsScope(scopes []string, scope string) bool { - for _, item := range scopes { - if item == scope { - return true - } - } - return false + return slices.Contains(scopes, scope) } diff --git a/internal/apps/oauth/cache.go b/internal/apps/oauth/cache.go index 266f29e2..be1fe552 100644 --- a/internal/apps/oauth/cache.go +++ b/internal/apps/oauth/cache.go @@ -5,6 +5,7 @@ package oauth import ( "context" + "errors" "fmt" "strconv" "sync" @@ -31,10 +32,12 @@ var ( tokenListenerOnce sync.Once tokenListenerCtx context.Context tokenListenerCancel context.CancelFunc + tokenListenerDone chan struct{} userListenerOnce sync.Once userListenerCtx context.Context userListenerCancel context.CancelFunc + userListenerDone chan struct{} ) func tokenCacheKey(tokenHash string) string { @@ -54,15 +57,19 @@ func ensureTokenCacheListener() { func startTokenCacheInvalidationListener() { tokenListenerCtx, tokenListenerCancel = context.WithCancel(context.Background()) + tokenListenerDone = make(chan struct{}) go func() { - pubsub := db.Redis.Subscribe(tokenListenerCtx, oauthTokenInvalidationChannel) + listenerCtx := tokenListenerCtx + defer close(tokenListenerDone) + + pubsub := db.Redis.Subscribe(listenerCtx, oauthTokenInvalidationChannel) defer func() { _ = pubsub.Close() }() go func() { - <-tokenListenerCtx.Done() + <-listenerCtx.Done() _ = pubsub.Close() }() @@ -93,15 +100,19 @@ func ensureUserCacheListener() { func startUserCacheInvalidationListener() { userListenerCtx, userListenerCancel = context.WithCancel(context.Background()) + userListenerDone = make(chan struct{}) go func() { - pubsub := db.Redis.Subscribe(userListenerCtx, oauthUserInvalidationChannel) + listenerCtx := userListenerCtx + defer close(userListenerDone) + + pubsub := db.Redis.Subscribe(listenerCtx, oauthUserInvalidationChannel) defer func() { _ = pubsub.Close() }() go func() { - <-userListenerCtx.Done() + <-listenerCtx.Done() _ = pubsub.Close() }() @@ -140,7 +151,7 @@ func GetCachedToken(ctx context.Context, tokenHash string) (*model.AccessToken, return &token, nil } } - return nil, fmt.Errorf("cache miss") + return nil, errors.New("cache miss") } // SetCachedToken 设置 AccessToken 缓存 @@ -183,7 +194,7 @@ func GetCachedUser(ctx context.Context, userID uint64) (*model.User, error) { return &u, nil } } - return nil, fmt.Errorf("cache miss") + return nil, errors.New("cache miss") } // SetCachedUser 设置 User 缓存 @@ -213,13 +224,21 @@ func InvalidateCachedUser(ctx context.Context, userID uint64) { func StopOauthCacheListener() { if tokenListenerCancel != nil { tokenListenerCancel() + if tokenListenerDone != nil { + <-tokenListenerDone + } tokenListenerCancel = nil + tokenListenerDone = nil } tokenListenerOnce = sync.Once{} if userListenerCancel != nil { userListenerCancel() + if userListenerDone != nil { + <-userListenerDone + } userListenerCancel = nil + userListenerDone = nil } userListenerOnce = sync.Once{} } diff --git a/internal/apps/oauth/oauth_test.go b/internal/apps/oauth/oauth_test.go index 1ae83bb0..eca5d6fe 100644 --- a/internal/apps/oauth/oauth_test.go +++ b/internal/apps/oauth/oauth_test.go @@ -336,6 +336,7 @@ func newMockOIDCClient(issuer, clientID string, expectedState *string, sub, user } func setupTestDB(t *testing.T) *gorm.DB { + t.Helper() repository.ResetSystemConfigRAMCacheForTest() repository.ResetAuthSourceRAMCacheForTest() @@ -380,6 +381,11 @@ func resetOIDCProviderCacheForTest() { } func setupTestRouter(dbConn *gorm.DB, mockRedis *mockRedisClient, mockClient *http.Client) *gin.Engine { + // 停止各层 Pub/Sub 监听 goroutine(可能在先前测试的 API 调用中随 sync.Once + // 启动),否则它们在 db.Redis 被替换时仍读取旧值,产生数据竞争。 + StopOauthCacheListener() + repository.StopAuthSourceCacheListener() + repository.StopSystemConfigCacheListener() resetOIDCProviderCacheForTest() r := testhelper.NewTestGinEngine(gin.Recovery()) diff --git a/internal/apps/openflare/agent/observability.go b/internal/apps/openflare/agent/observability.go index 0b81b633..0e6cf8ac 100644 --- a/internal/apps/openflare/agent/observability.go +++ b/internal/apps/openflare/agent/observability.go @@ -179,18 +179,9 @@ func buildNodeAccessLogRecords(nodeID string, direct []NodeAccessLog, buffered [ records := make([]*model.OpenFlareAccessLog, 0, total) appendLogs := func(logs []NodeAccessLog) { for _, item := range logs { - bytesSent := item.BytesSent - if bytesSent < 0 { - bytesSent = 0 - } - requestLength := item.RequestLength - if requestLength < 0 { - requestLength = 0 - } - requestTimeMs := item.RequestTimeMs - if requestTimeMs < 0 { - requestTimeMs = 0 - } + bytesSent := max(item.BytesSent, 0) + requestLength := max(item.RequestLength, 0) + requestTimeMs := max(item.RequestTimeMs, 0) record := &model.OpenFlareAccessLog{ NodeID: nodeID, LoggedAt: timeFromUnix(item.LoggedAtUnix, reportedAt), diff --git a/internal/apps/openflare/agent/waf_ip_group.go b/internal/apps/openflare/agent/waf_ip_group.go index af0de03b..a13886a7 100644 --- a/internal/apps/openflare/agent/waf_ip_group.go +++ b/internal/apps/openflare/agent/waf_ip_group.go @@ -11,6 +11,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "sort" "strconv" "strings" @@ -45,7 +46,7 @@ func ChangedWAFIPGroupsForAgent(ctx context.Context, ids []uint, checksums map[s } changed := make([]WAFIPGroup, 0, len(groups)) for _, group := range groups { - if strings.TrimSpace(checksums[fmt.Sprintf("%d", group.ID)]) == group.Checksum { + if strings.TrimSpace(checksums[strconv.FormatUint(uint64(group.ID), 10)]) == group.Checksum { continue } changed = append(changed, group) @@ -97,7 +98,7 @@ func buildAgentWAFIPGroups(ctx context.Context, ids []uint) ([]WAFIPGroup, error if len(ids) == 0 { return []WAFIPGroup{}, nil } - sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) + slices.Sort(ids) groups, err := repository.ListOpenFlareWAFIPGroupsByIDs(ctx, ids) if err != nil { return nil, err @@ -204,7 +205,7 @@ func activeConfigWAFIPGroupIDs(ctx context.Context) ([]uint, error) { for id := range idSet { ids = append(ids, id) } - sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) + slices.Sort(ids) return ids, nil } diff --git a/internal/apps/openflare/apply_log/logics_test.go b/internal/apps/openflare/apply_log/logics_test.go index eec5dc98..06b38722 100644 --- a/internal/apps/openflare/apply_log/logics_test.go +++ b/internal/apps/openflare/apply_log/logics_test.go @@ -19,6 +19,7 @@ import ( ) func setupApplyLogTestDB(t *testing.T) func() { + t.Helper() sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ DisableForeignKeyConstraintWhenMigrating: true, }) diff --git a/internal/apps/openflare/async_tasks.go b/internal/apps/openflare/async_tasks.go index 626ecc11..7a4944d3 100644 --- a/internal/apps/openflare/async_tasks.go +++ b/internal/apps/openflare/async_tasks.go @@ -1,3 +1,6 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + // Package openflare implements openflare configuration, service orchestration, and background tasks. package openflare diff --git a/internal/apps/openflare/cloudflare/client.go b/internal/apps/openflare/cloudflare/client.go index 4190bd7d..0d141cc1 100644 --- a/internal/apps/openflare/cloudflare/client.go +++ b/internal/apps/openflare/cloudflare/client.go @@ -192,7 +192,7 @@ func (client *HTTPClient) do(ctx context.Context, method, path string, query url return err } requestURL := buildRequestURL(client.baseURL, path, query) - for attempt := 0; attempt < maxRequestAttempts; attempt++ { + for attempt := range maxRequestAttempts { statusCode, retryHeader, responseBody, requestErr := client.send(ctx, method, requestURL, encodedBody) if requestErr != nil { return requestErr diff --git a/internal/apps/openflare/config_version/certificate_snapshot_test.go b/internal/apps/openflare/config_version/certificate_snapshot_test.go index de536c17..ea270101 100644 --- a/internal/apps/openflare/config_version/certificate_snapshot_test.go +++ b/internal/apps/openflare/config_version/certificate_snapshot_test.go @@ -94,6 +94,7 @@ func TestBuildSnapshotReadsZoneDomainCertificates(t *testing.T) { } func generateTestCertKeyPairForSnapshot(t *testing.T) (certPEM string, keyPEM string) { + t.Helper() return generateTestCertKeyPairForSnapshotForDomain(t, "test.example.com") } diff --git a/internal/apps/openflare/config_version/helpers.go b/internal/apps/openflare/config_version/helpers.go index 49c8b3a2..1c776471 100644 --- a/internal/apps/openflare/config_version/helpers.go +++ b/internal/apps/openflare/config_version/helpers.go @@ -6,6 +6,7 @@ package config_version import ( "context" "encoding/json" + "errors" "fmt" "net" "strconv" @@ -27,7 +28,7 @@ func normalizeSnapshotDomains(domains []string) ([]string, error) { for _, raw := range domains { domain := strings.ToLower(strings.TrimSpace(raw)) if domain == "" || strings.Contains(domain, "://") || strings.Contains(domain, "/") { - return nil, fmt.Errorf("domains payload is invalid") + return nil, errors.New("domains payload is invalid") } if _, ok := seen[domain]; ok { continue @@ -36,7 +37,7 @@ func normalizeSnapshotDomains(domains []string) ([]string, error) { normalized = append(normalized, domain) } if len(normalized) == 0 { - return nil, fmt.Errorf("domain is required") + return nil, errors.New("domain is required") } return normalized, nil } @@ -55,7 +56,7 @@ func decodeStoredUpstreams(raw string, fallbackOriginURL string) ([]string, erro } var upstreams []string if err := json.Unmarshal([]byte(text), &upstreams); err != nil { - return nil, fmt.Errorf("upstreams payload is invalid") + return nil, errors.New("upstreams payload is invalid") } return normalizeUpstreams(fallbackOriginURL, upstreams) } @@ -79,7 +80,7 @@ func normalizeUpstreams(originURL string, upstreams []string) ([]string, error) normalized = append(normalized, value) } if len(normalized) == 0 { - return nil, fmt.Errorf("upstream is required") + return nil, errors.New("upstream is required") } return normalized, nil } @@ -91,7 +92,7 @@ func decodeStoredCustomHeaders(raw string) ([]customHeaderInput, error) { } var headers []customHeaderInput if err := json.Unmarshal([]byte(text), &headers); err != nil { - return nil, fmt.Errorf("custom_headers payload is invalid") + return nil, errors.New("custom_headers payload is invalid") } return headers, nil } @@ -103,7 +104,7 @@ func decodeStoredCacheRules(raw string) ([]string, error) { } var rules []string if err := json.Unmarshal([]byte(text), &rules); err != nil { - return nil, fmt.Errorf("cache_rules payload is invalid") + return nil, errors.New("cache_rules payload is invalid") } normalized := make([]string, 0, len(rules)) for _, rule := range rules { diff --git a/internal/apps/openflare/config_version/logics.go b/internal/apps/openflare/config_version/logics.go index 973608dc..e7828b14 100644 --- a/internal/apps/openflare/config_version/logics.go +++ b/internal/apps/openflare/config_version/logics.go @@ -494,51 +494,51 @@ func diffOpenRestyOptionDetails(left openRestyConfigSnapshot, right openRestyCon CurrentValue: current, }) } - appendIfChanged("OpenRestyDefaultServerReturnStatus", fmt.Sprintf("%d", left.DefaultServerReturnStatus), fmt.Sprintf("%d", right.DefaultServerReturnStatus)) + appendIfChanged("OpenRestyDefaultServerReturnStatus", strconv.Itoa(left.DefaultServerReturnStatus), strconv.Itoa(right.DefaultServerReturnStatus)) appendIfChanged("OpenRestyWorkerProcesses", left.WorkerProcesses, right.WorkerProcesses) - appendIfChanged("OpenRestyWorkerConnections", fmt.Sprintf("%d", left.WorkerConnections), fmt.Sprintf("%d", right.WorkerConnections)) - appendIfChanged("OpenRestyWorkerRlimitNofile", fmt.Sprintf("%d", left.WorkerRlimitNofile), fmt.Sprintf("%d", right.WorkerRlimitNofile)) + appendIfChanged("OpenRestyWorkerConnections", strconv.Itoa(left.WorkerConnections), strconv.Itoa(right.WorkerConnections)) + appendIfChanged("OpenRestyWorkerRlimitNofile", strconv.Itoa(left.WorkerRlimitNofile), strconv.Itoa(right.WorkerRlimitNofile)) appendIfChanged("OpenRestyEventsUse", left.EventsUse, right.EventsUse) - appendIfChanged("OpenRestyEventsMultiAcceptEnabled", fmt.Sprintf("%t", left.EventsMultiAcceptEnabled), fmt.Sprintf("%t", right.EventsMultiAcceptEnabled)) - appendIfChanged("OpenRestyKeepaliveTimeout", fmt.Sprintf("%d", left.KeepaliveTimeout), fmt.Sprintf("%d", right.KeepaliveTimeout)) - appendIfChanged("OpenRestyKeepaliveRequests", fmt.Sprintf("%d", left.KeepaliveRequests), fmt.Sprintf("%d", right.KeepaliveRequests)) - appendIfChanged("OpenRestyClientHeaderTimeout", fmt.Sprintf("%d", left.ClientHeaderTimeout), fmt.Sprintf("%d", right.ClientHeaderTimeout)) - appendIfChanged("OpenRestyClientBodyTimeout", fmt.Sprintf("%d", left.ClientBodyTimeout), fmt.Sprintf("%d", right.ClientBodyTimeout)) + appendIfChanged("OpenRestyEventsMultiAcceptEnabled", strconv.FormatBool(left.EventsMultiAcceptEnabled), strconv.FormatBool(right.EventsMultiAcceptEnabled)) + appendIfChanged("OpenRestyKeepaliveTimeout", strconv.Itoa(left.KeepaliveTimeout), strconv.Itoa(right.KeepaliveTimeout)) + appendIfChanged("OpenRestyKeepaliveRequests", strconv.Itoa(left.KeepaliveRequests), strconv.Itoa(right.KeepaliveRequests)) + appendIfChanged("OpenRestyClientHeaderTimeout", strconv.Itoa(left.ClientHeaderTimeout), strconv.Itoa(right.ClientHeaderTimeout)) + appendIfChanged("OpenRestyClientBodyTimeout", strconv.Itoa(left.ClientBodyTimeout), strconv.Itoa(right.ClientBodyTimeout)) appendIfChanged("OpenRestyClientMaxBodySize", left.ClientMaxBodySize, right.ClientMaxBodySize) appendIfChanged("OpenRestyLargeClientHeaderBuffers", left.LargeClientHeaderBuffers, right.LargeClientHeaderBuffers) - appendIfChanged("OpenRestySendTimeout", fmt.Sprintf("%d", left.SendTimeout), fmt.Sprintf("%d", right.SendTimeout)) - appendIfChanged("OpenRestyProxyConnectTimeout", fmt.Sprintf("%d", left.ProxyConnectTimeout), fmt.Sprintf("%d", right.ProxyConnectTimeout)) - appendIfChanged("OpenRestyProxySendTimeout", fmt.Sprintf("%d", left.ProxySendTimeout), fmt.Sprintf("%d", right.ProxySendTimeout)) - appendIfChanged("OpenRestyProxyReadTimeout", fmt.Sprintf("%d", left.ProxyReadTimeout), fmt.Sprintf("%d", right.ProxyReadTimeout)) - appendIfChanged("OpenRestyWebsocketEnabled", fmt.Sprintf("%t", left.WebsocketEnabled), fmt.Sprintf("%t", right.WebsocketEnabled)) - appendIfChanged("OpenRestyHTTP3Enabled", fmt.Sprintf("%t", left.HTTP3Enabled), fmt.Sprintf("%t", right.HTTP3Enabled)) - appendIfChanged("OpenRestyProxyRequestBufferingEnabled", fmt.Sprintf("%t", left.ProxyRequestBuffering), fmt.Sprintf("%t", right.ProxyRequestBuffering)) - appendIfChanged("OpenRestyProxyBufferingEnabled", fmt.Sprintf("%t", left.ProxyBufferingEnabled), fmt.Sprintf("%t", right.ProxyBufferingEnabled)) + appendIfChanged("OpenRestySendTimeout", strconv.Itoa(left.SendTimeout), strconv.Itoa(right.SendTimeout)) + appendIfChanged("OpenRestyProxyConnectTimeout", strconv.Itoa(left.ProxyConnectTimeout), strconv.Itoa(right.ProxyConnectTimeout)) + appendIfChanged("OpenRestyProxySendTimeout", strconv.Itoa(left.ProxySendTimeout), strconv.Itoa(right.ProxySendTimeout)) + appendIfChanged("OpenRestyProxyReadTimeout", strconv.Itoa(left.ProxyReadTimeout), strconv.Itoa(right.ProxyReadTimeout)) + appendIfChanged("OpenRestyWebsocketEnabled", strconv.FormatBool(left.WebsocketEnabled), strconv.FormatBool(right.WebsocketEnabled)) + appendIfChanged("OpenRestyHTTP3Enabled", strconv.FormatBool(left.HTTP3Enabled), strconv.FormatBool(right.HTTP3Enabled)) + appendIfChanged("OpenRestyProxyRequestBufferingEnabled", strconv.FormatBool(left.ProxyRequestBuffering), strconv.FormatBool(right.ProxyRequestBuffering)) + appendIfChanged("OpenRestyProxyBufferingEnabled", strconv.FormatBool(left.ProxyBufferingEnabled), strconv.FormatBool(right.ProxyBufferingEnabled)) appendIfChanged("OpenRestyProxyBuffers", left.ProxyBuffers, right.ProxyBuffers) appendIfChanged("OpenRestyProxyBufferSize", left.ProxyBufferSize, right.ProxyBufferSize) appendIfChanged("OpenRestyProxyBusyBuffersSize", left.ProxyBusyBuffersSize, right.ProxyBusyBuffersSize) - appendIfChanged("OpenRestyGzipEnabled", fmt.Sprintf("%t", left.GzipEnabled), fmt.Sprintf("%t", right.GzipEnabled)) - appendIfChanged("OpenRestyGzipMinLength", fmt.Sprintf("%d", left.GzipMinLength), fmt.Sprintf("%d", right.GzipMinLength)) - appendIfChanged("OpenRestyGzipCompLevel", fmt.Sprintf("%d", left.GzipCompLevel), fmt.Sprintf("%d", right.GzipCompLevel)) + appendIfChanged("OpenRestyGzipEnabled", strconv.FormatBool(left.GzipEnabled), strconv.FormatBool(right.GzipEnabled)) + appendIfChanged("OpenRestyGzipMinLength", strconv.Itoa(left.GzipMinLength), strconv.Itoa(right.GzipMinLength)) + appendIfChanged("OpenRestyGzipCompLevel", strconv.Itoa(left.GzipCompLevel), strconv.Itoa(right.GzipCompLevel)) appendIfChanged("OpenRestyResolvers", left.Resolvers, right.Resolvers) - appendIfChanged("OpenRestyCacheEnabled", fmt.Sprintf("%t", left.CacheEnabled), fmt.Sprintf("%t", right.CacheEnabled)) + appendIfChanged("OpenRestyCacheEnabled", strconv.FormatBool(left.CacheEnabled), strconv.FormatBool(right.CacheEnabled)) appendIfChanged("OpenRestyCachePath", left.CachePath, right.CachePath) appendIfChanged("OpenRestyCacheLevels", left.CacheLevels, right.CacheLevels) appendIfChanged("OpenRestyCacheInactive", left.CacheInactive, right.CacheInactive) appendIfChanged("OpenRestyCacheMaxSize", left.CacheMaxSize, right.CacheMaxSize) appendIfChanged("OpenRestyCacheKeyTemplate", left.CacheKeyTemplate, right.CacheKeyTemplate) - appendIfChanged("OpenRestyCacheLockEnabled", fmt.Sprintf("%t", left.CacheLockEnabled), fmt.Sprintf("%t", right.CacheLockEnabled)) + appendIfChanged("OpenRestyCacheLockEnabled", strconv.FormatBool(left.CacheLockEnabled), strconv.FormatBool(right.CacheLockEnabled)) appendIfChanged("OpenRestyCacheLockTimeout", left.CacheLockTimeout, right.CacheLockTimeout) appendIfChanged("OpenRestyCacheUseStale", left.CacheUseStale, right.CacheUseStale) - appendIfChanged("OpenRestyDefaultLimitConnPerServer", fmt.Sprintf("%d", left.DefaultLimitConnPerServer), fmt.Sprintf("%d", right.DefaultLimitConnPerServer)) - appendIfChanged("OpenRestyDefaultLimitConnPerIP", fmt.Sprintf("%d", left.DefaultLimitConnPerIP), fmt.Sprintf("%d", right.DefaultLimitConnPerIP)) + appendIfChanged("OpenRestyDefaultLimitConnPerServer", strconv.Itoa(left.DefaultLimitConnPerServer), strconv.Itoa(right.DefaultLimitConnPerServer)) + appendIfChanged("OpenRestyDefaultLimitConnPerIP", strconv.Itoa(left.DefaultLimitConnPerIP), strconv.Itoa(right.DefaultLimitConnPerIP)) appendIfChanged("OpenRestyDefaultLimitRate", left.DefaultLimitRate, right.DefaultLimitRate) appendIfChanged("OpenRestyDefaultLimitReqPerIP", left.DefaultLimitReqPerIP, right.DefaultLimitReqPerIP) - appendIfChanged("OriginErrorPageEnabled", fmt.Sprintf("%t", left.OriginErrorPageEnabled), fmt.Sprintf("%t", right.OriginErrorPageEnabled)) + appendIfChanged("OriginErrorPageEnabled", strconv.FormatBool(left.OriginErrorPageEnabled), strconv.FormatBool(right.OriginErrorPageEnabled)) appendIfChanged("OriginErrorPageStatusCodes", encodeOriginErrorPageStatusCodes(left.OriginErrorPageStatusCodes), encodeOriginErrorPageStatusCodes(right.OriginErrorPageStatusCodes)) appendIfChanged("OriginErrorPageHTML", left.OriginErrorPageHTML, right.OriginErrorPageHTML) - appendIfChanged("OriginErrorPageGetOnly", fmt.Sprintf("%t", left.OriginErrorPageGetOnly), fmt.Sprintf("%t", right.OriginErrorPageGetOnly)) - appendIfChanged("SWOfflineEnabled", fmt.Sprintf("%t", left.SWOfflineEnabled), fmt.Sprintf("%t", right.SWOfflineEnabled)) + appendIfChanged("OriginErrorPageGetOnly", strconv.FormatBool(left.OriginErrorPageGetOnly), strconv.FormatBool(right.OriginErrorPageGetOnly)) + appendIfChanged("SWOfflineEnabled", strconv.FormatBool(left.SWOfflineEnabled), strconv.FormatBool(right.SWOfflineEnabled)) appendIfChanged("SWOfflineHTML", left.SWOfflineHTML, right.SWOfflineHTML) appendIfChanged("SWOfflineDomains", encodeSWOfflineDomains(left.SWOfflineDomains), encodeSWOfflineDomains(right.SWOfflineDomains)) return changes diff --git a/internal/apps/openflare/config_version/origin_error_page_snapshot_test.go b/internal/apps/openflare/config_version/origin_error_page_snapshot_test.go index 140c3f50..3e9dd785 100644 --- a/internal/apps/openflare/config_version/origin_error_page_snapshot_test.go +++ b/internal/apps/openflare/config_version/origin_error_page_snapshot_test.go @@ -98,7 +98,7 @@ func TestDiffOpenRestyOptionDetailsOriginErrorPage(t *testing.T) { assert.Equal(t, "false", keys["OriginErrorPageEnabled"].CurrentValue) assert.Equal(t, `["500-599"]`, keys["OriginErrorPageStatusCodes"].PreviousValue) assert.Equal(t, `["522"]`, keys["OriginErrorPageStatusCodes"].CurrentValue) - assert.Equal(t, "", keys["OriginErrorPageHTML"].PreviousValue) + assert.Empty(t, keys["OriginErrorPageHTML"].PreviousValue) assert.Equal(t, "

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 {