Skip to content

feat: 缓存 TTL 面板可调 + 全量审计修复 - #10

Merged
wha7ev9r merged 5 commits into
mainfrom
feature/ttl-panel-control
Jul 31, 2026
Merged

feat: 缓存 TTL 面板可调 + 全量审计修复#10
wha7ev9r merged 5 commits into
mainfrom
feature/ttl-panel-control

Conversation

@wha7ev9r

@wha7ev9r wha7ev9r commented Jul 31, 2026

Copy link
Copy Markdown
Owner

功能

  • 镜像缓存 TTL 与 Git 代理缓存 TTL 均可在控制台面板配置(0 = 永久缓存 / 禁用缓存),保存即生效并持久化
  • 新增 GET/PUT /api/config/gitproxy 端点,Git 代理 TTL 面板带"新 TTL 仅对之后写入的缓存生效"提示
  • HuggingFace 镜像默认 TTL 7d → 0(永久缓存);NuGet 默认地址改为 https://api.nuget.org/v3(健康检查自动补 /index.json)

审计修复

缓存层

  • Set 写入失败清理残留文件、旧 .exp 删除、成功后才记账;缓存 key 纳入 HTTP 方法;仅 GET 读写缓存
  • 17 个镜像 TTL 输出支持秒级格式;upstream 尾部斜杠统一

服务层

  • 面板保存的 auth_token 不再落盘(env 注入 token 不写回配置文件);config 原子写入;热重载失败重试
  • registry token 不缓存临期 token;限流中间件始终装配,/api/search、release 接口纳入限流
  • 静态资源 SPA fallback 直读 index.html(修复深链接如 /mirrors 被 301 到根页的问题)+ 路径遍历防护
  • 镜像 TTL/时长/缓存大小溢出校验;CIDR 解析失败告警;webhook 非 2xx 视为失败
  • release 下载 4xx 状态透传;Content-Length 仅 HEAD 预设,避免与真实内容不一致
  • 流量统计排序确定性修复

前端

  • 镜像 TTL 保存失败自动回滚;Git 代理 TTL 面板;搜索竞态守卫;限流表单前后端一致校验;登录后回跳原页面(防开放重定向);无效日期渲染容错;日志刷新容错

验证

  • go build ./... + go test ./internal/... 全通过
  • pnpm run build(vue-tsc + vite)通过
  • 冒烟:/api/config/gitproxy PUT/GET、非法 TTL 400、镜像 TTL 持久化、/api/config/ratelimit 非法 interval 400
  • Edge headless 截图确认镜像源页面(每行 TTL 下拉、不缓存镜像提示、Git 代理 TTL 面板)渲染正常

Summary by CodeRabbit

  • New Features

    • Added configurable cache TTLs for individual mirrors and Git proxy caching.
    • Added Hugging Face, Conda, RubyGems, Cargo, and NuGet mirror defaults.
    • Login redirects now preserve the originally requested path.
  • Bug Fixes

    • Improved cache cleanup, expiry handling, and size accounting.
    • Fixed stale search results, invalid date display, and mirror URL handling.
    • Improved webhook, upstream, dashboard, and rate-limit error handling.
  • Documentation

    • Updated NuGet mirror endpoint examples and configuration documentation.

wha7ev9r added 3 commits July 31, 2026 20:59
- 镜像缓存 TTL 与 Git 代理缓存 TTL 可在面板配置(0=永久/禁用)
- GitProxy 增加 CacheTTL 读写接口与 /api/config/gitproxy 端点
- config 保存防 env token 落盘(AuthTokenOrig)、原子写入、mtime 失败重试
- token 缓存不缓存临期 token;限流中间件始终装配且 search/release 纳入限流
- 静态资源 SPA fallback 直读 index.html,修复深链接被 301 到根页的问题
- 路径遍历防护、CIDR/时长/大小溢出校验、webhook 非 2xx 视为失败
- 前端:搜索竞态守卫、表单校验、登录回跳、日期守卫、日志容错
- Set 删除过期 TTL 文件、失败清理残留、成功后才记账
- 缓存 key 纳入方法;仅 GET 读写缓存,401 重试仅限 GET/无 body
- 17 个镜像 TTL 输出支持 %ds 秒级格式;upstream 尾部斜杠统一
- nuget 默认地址改为 /v3,健康检查自动补 /index.json
- alert webhook 非 2xx 视为失败;CIDR 解析失败告警
- 限流 rate/window 非法时放行而非误杀;PurgeOldTraffic 防护
- release 下载 4xx 透传、Content-Length 仅 HEAD 预设
- 流量统计排序确定性(mirror 排序、周聚合按表达式分组)
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5f080109-8b33-410c-a1fe-1581b9f699d0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds configurable cache TTLs for mirrors and the Git proxy. It updates cache accounting, configuration persistence, dashboard APIs, frontend controls, authentication redirects, proxy responses, rate limiting, and request-processing edge cases.

Changes

Configurable cache TTL and proxy flow

Layer / File(s) Summary
Configuration and mirror TTL contracts
README.md, configs/devbox.yaml, internal/config/config.go, internal/mirror/*
Mirror defaults and NuGet endpoints are updated. TTL formatting preserves seconds for non-minute durations. Proxy handlers trim trailing slashes.
Cache storage and Git proxy TTL handling
internal/mirror/cache.go, internal/mirror/cache_test.go, internal/gitproxy/gitproxy.go
Cache keys include HTTP methods. Only eligible GET requests use the cache. Replacement accounting and metadata cleanup are updated. Git proxy TTL access is synchronized.
Dashboard API and server wiring
internal/dashboard/dashboard.go, internal/server/server.go
Authenticated GET and PUT handlers expose Git proxy TTL settings. The server connects these handlers to runtime state and configuration persistence.
Server and edge handling
internal/alert/alert.go, internal/dashboard/release.go, internal/ratelimit/ratelimit.go, internal/server/server.go, internal/store/sqlite.go
Webhook status errors, release responses, rate-limit handling, retries, token caching, reload behavior, metrics, response status tracking, and traffic retention are updated.
Frontend controls and request handling
web/src/api/client.ts, web/src/router/index.ts, web/src/views/Login.vue, web/src/views/Mirrors.vue, web/src/views/Dashboard.vue, web/src/views/Releases.vue, web/src/views/Search.vue, web/src/views/Settings.vue
The frontend adds TTL controls, safe login redirects, error handling, date validation, stale-response protection, and rate-limit validation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant MirrorsVue
  participant GitProxyConfigAPI
  participant Dashboard
  participant Server
  participant Config
  Browser->>MirrorsVue: Select Git proxy cache TTL
  MirrorsVue->>GitProxyConfigAPI: PUT cacheTTL
  GitProxyConfigAPI->>Dashboard: Send authenticated request
  Dashboard->>Server: SetCacheTTL(ttl)
  Server->>Config: Persist Git proxy TTL
  Config-->>Server: Return save result
  Server-->>Dashboard: Return updated TTL
  Dashboard-->>MirrorsVue: Return JSON response
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: adding adjustable cache TTL panel configuration and completing comprehensive audit fixes across the caching layer, service layer, and frontend.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/ttl-panel-control

Comment @coderabbitai help to get the list of available commands.

Comment thread web/src/views/Login.vue Fixed
Comment thread internal/server/server.go
// Path escapes the frontend dir (".." traversal): fall back.
full = filepath.Join(frontDir, "index.html")
}
if _, err := os.Stat(full); err != nil {
Comment thread web/src/views/Login.vue
// navigates to an external host.
const redirect = new URLSearchParams(window.location.search).get('redirect')
const safe = redirect && redirect.startsWith('/') && !redirect.startsWith('//')
window.location.href = safe ? redirect : '/'
Comment thread web/src/views/Login.vue
// navigates to an external host.
const redirect = new URLSearchParams(window.location.search).get('redirect')
const safe = redirect && redirect.startsWith('/') && !redirect.startsWith('//')
window.location.href = safe ? redirect : '/'

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
internal/mirror/cache.go (2)

92-145: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Reclaim usedBytes when a write fails after replacing an existing entry.

When header creation (line 118) or expiry-write (lines 133-136) fails, Set removes the newly written file and returns before the usedBytes update at line 145. Since os.Create (line 102) already truncated and replaced any prior file at path, the entry is now gone from disk, but usedBytes still holds the old oldSize contribution counted during a previous successful Set call. This permanently overcounts usedBytes by oldSize after each such failure.

Decrement oldSize from usedBytes in these early-return paths so the counter matches the disk state.

🔧 Proposed fix
 	hf, err := os.Create(hdrPath)
 	if err != nil {
 		slog.Warn("cache header write error", "path", hdrPath, "error", err)
 		os.Remove(path)
+		c.usedBytes -= oldSize
 		return
 	}
@@
 		if err != nil {
 			slog.Warn("cache expiry write error", "path", expPath, "error", err)
 			os.Remove(path)
 			os.Remove(hdrPath)
+			c.usedBytes -= oldSize
 			return
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/mirror/cache.go` around lines 92 - 145, Update the header-creation
and expiry-write failure paths in Set to subtract oldSize from c.usedBytes
before returning, after removing the replaced cache files. Preserve the existing
cleanup and logging behavior, and do not alter the successful final usedBytes
adjustment.

297-311: 🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

usedBytes no longer tracks real disk usage; fix the stat/rename ordering and propagate writeCacheMeta failures.

Two issues combine here to break the cache's disk-quota accounting:

  1. os.Stat(path) (line 303) runs AFTER os.Rename (line 299). At that point, path already holds the NEW file, so info.Size() equals written, not the size of the file being replaced. The result is c.usedBytes += written - written = 0 for every write, new or replaced. Set() (lines 92-95) captures oldSize BEFORE overwriting, which is the correct pattern this block should follow.
  2. writeCacheMeta (line 306) can delete path on failure (lines 373, 388-389) but has no return value, so c.usedBytes += written (line 307) still executes even when the file no longer exists, permanently inflating the counter.

Together, usedBytes stops reflecting actual disk usage almost immediately after startup. This makes the maxBytes-based evictLRU() trigger unreliably, letting the cache directory grow unbounded on disk, or evicting valid entries based on phantom byte counts.

🔧 Proposed fix
 	c.mu.Lock()
 	defer c.mu.Unlock()
+	var oldSize int64
+	if info, err := os.Stat(path); err == nil {
+		oldSize = info.Size()
+	}
 	if err := os.Rename(tmp.Name(), path); err != nil {
 		slog.Warn("cache rename error", "path", path, "error", err)
 		return
 	}
-	if info, err := os.Stat(path); err == nil {
-		c.usedBytes -= info.Size()
-	}
-	c.writeCacheMeta(path, respHdr, ttl)
-	c.usedBytes += written
+	if !c.writeCacheMeta(path, respHdr, ttl) {
+		// writeCacheMeta already removed the cache file on failure.
+		c.usedBytes -= oldSize
+		return
+	}
+	c.usedBytes += written - oldSize
 	if c.maxBytes > 0 && c.usedBytes > c.maxBytes {
 		c.evictLRU()
 	}

This requires writeCacheMeta to return a bool (or error) indicating success, with return false on each early-return path and return true at the end.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/mirror/cache.go` around lines 297 - 311, Fix cache accounting in the
write flow around writeCacheMeta: stat and record the existing path size before
os.Rename, then subtract that old size before adding written. Change
writeCacheMeta to return success status, returning failure on every early exit
and success only after metadata is written; update the caller to stop accounting
and return when metadata writing fails, so usedBytes reflects files that remain
on disk.
internal/gitproxy/gitproxy.go (1)

123-148: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Use the locked local cacheTTL, not gp.cacheTTL, in proxyArchive.

Line 145 calls gp.cache.ProxyHTTP(w, r, upstream, gp.cacheTTL), reading gp.cacheTTL directly and unsynchronized. SetCacheTTL can mutate gp.cacheTTL concurrently under gp.mu.Lock(), so this is a data race. proxyRaw (line 188) and proxySmartHTTP (line 216) both correctly capture cacheTTL under gp.mu.RLock() at the top of the function and reuse that local variable throughout; proxyArchive does the same capture at line 125 but then reverts to the unguarded field read at line 145.

This also means the value used to build the request may differ from the value that gated the cacheTTL > 0 check at line 127, if SetCacheTTL runs in between.

🔧 Proposed fix
 		orig := r.URL.Path
 		r.URL.Path = path
-		gp.cache.ProxyHTTP(w, r, upstream, gp.cacheTTL)
+		gp.cache.ProxyHTTP(w, r, upstream, cacheTTL)
 		r.URL.Path = orig
 		return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/gitproxy/gitproxy.go` around lines 123 - 148, Update proxyArchive to
pass the already captured, lock-protected local cacheTTL to gp.cache.ProxyHTTP
instead of reading gp.cacheTTL directly, ensuring the request uses the same
value as the cacheTTL > 0 check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/ratelimit/ratelimit.go`:
- Around line 47-51: Move the degenerate-config guard in Allow so blacklist
evaluation runs first, ensuring blacklisted IPs are rejected even when limit or
window is non-positive; preserve the existing guard behavior for non-blacklisted
requests.

In `@internal/server/server.go`:
- Around line 816-825: Update SetCacheTTL to acquire s.cfgMu.RLock(), copy
s.gitProxy to a local variable, release the read lock, and invoke CacheTTL
mutation on that local proxy, matching GetCacheTTL and gitProxyHandler while
avoiding access during concurrent config reloads.

In `@web/src/views/Dashboard.vue`:
- Around line 211-216: Update refreshLogs so that after getRecentLogs succeeds
and logs.value is updated, errorMsg.value is cleared; preserve the existing
catch behavior for failed requests.
- Around line 211-216: Update refreshLogs to sequence overlapping requests with
a monotonically increasing request ID, following the existing
granularityRequestId pattern. Only apply logs.value or errorMsg.value when the
response or error belongs to the latest request, so stale results cannot
overwrite newer state.

In `@web/src/views/Login.vue`:
- Around line 18-20: Update the redirect validation in the Login.vue redirect
assignment to accept only paths starting with a single forward slash; reject
values beginning with `//` or `/\` before assigning window.location.href, while
preserving the fallback to `/` for invalid or missing redirects.

In `@web/src/views/Mirrors.vue`:
- Around line 51-57: The ttlText function rounds TTL values to higher units,
causing precision loss—for example, 90 seconds becomes "2 分钟" instead of
preserving the exact input. Update the ttlText function to only convert to a
higher unit (days, hours, minutes) when the duration divides evenly; otherwise
return the value in seconds to preserve the exact configured value.
Additionally, update the selector option lists (referenced at the also-applies
lines) to include the current valid TTL as a selectable option so custom values
that don't align with the predefined options are properly represented.

In `@web/src/views/Settings.vue`:
- Around line 46-50: Normalize rlInterval.value with trim() once before
validation and reuse that normalized value when submitting the interval to the
server, ensuring inputs such as “3h ” are sent as “3h”. Update the relevant save
flow around the rlInterval validation and request payload while preserving the
existing validation and error behavior.

---

Outside diff comments:
In `@internal/gitproxy/gitproxy.go`:
- Around line 123-148: Update proxyArchive to pass the already captured,
lock-protected local cacheTTL to gp.cache.ProxyHTTP instead of reading
gp.cacheTTL directly, ensuring the request uses the same value as the cacheTTL >
0 check.

In `@internal/mirror/cache.go`:
- Around line 92-145: Update the header-creation and expiry-write failure paths
in Set to subtract oldSize from c.usedBytes before returning, after removing the
replaced cache files. Preserve the existing cleanup and logging behavior, and do
not alter the successful final usedBytes adjustment.
- Around line 297-311: Fix cache accounting in the write flow around
writeCacheMeta: stat and record the existing path size before os.Rename, then
subtract that old size before adding written. Change writeCacheMeta to return
success status, returning failure on every early exit and success only after
metadata is written; update the caller to stop accounting and return when
metadata writing fails, so usedBytes reflects files that remain on disk.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1d6eede1-8393-46c0-b048-0be6c4339e29

📥 Commits

Reviewing files that changed from the base of the PR and between 2d0b2ef and 2dc2f50.

📒 Files selected for processing (36)
  • README.md
  • configs/devbox.yaml
  • internal/alert/alert.go
  • internal/config/config.go
  • internal/dashboard/dashboard.go
  • internal/dashboard/release.go
  • internal/gitproxy/gitproxy.go
  • internal/mirror/alpine.go
  • internal/mirror/apt.go
  • internal/mirror/cache.go
  • internal/mirror/cargo.go
  • internal/mirror/conda.go
  • internal/mirror/cran.go
  • internal/mirror/docker.go
  • internal/mirror/ghcr.go
  • internal/mirror/github_api.go
  • internal/mirror/golang.go
  • internal/mirror/homebrew.go
  • internal/mirror/huggingface.go
  • internal/mirror/mcr.go
  • internal/mirror/npm.go
  • internal/mirror/nuget.go
  • internal/mirror/pypi.go
  • internal/mirror/quay.go
  • internal/mirror/rubygems.go
  • internal/ratelimit/ratelimit.go
  • internal/server/server.go
  • internal/store/sqlite.go
  • web/src/api/client.ts
  • web/src/router/index.ts
  • web/src/views/Dashboard.vue
  • web/src/views/Login.vue
  • web/src/views/Mirrors.vue
  • web/src/views/Releases.vue
  • web/src/views/Search.vue
  • web/src/views/Settings.vue

Comment thread internal/ratelimit/ratelimit.go Outdated
Comment thread internal/server/server.go
Comment thread web/src/views/Dashboard.vue
Comment thread web/src/views/Login.vue Outdated
Comment thread web/src/views/Mirrors.vue
Comment thread web/src/views/Settings.vue Outdated
@wha7ev9r

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@wha7ev9r
wha7ev9r merged commit 0a83712 into main Jul 31, 2026
8 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants