Skip to content

feat: persistent GitHub release download sources - #6

Closed
wha7ev9r wants to merge 4 commits into
mainfrom
feature/release-sources
Closed

feat: persistent GitHub release download sources#6
wha7ev9r wants to merge 4 commits into
mainfrom
feature/release-sources

Conversation

@wha7ev9r

@wha7ev9r wha7ev9r commented Jul 30, 2026

Copy link
Copy Markdown
Owner

概述

在面板新增 Releases 页面:一次添加 GitHub Release 源后长期保存,下次打开点击即可通过 DevBox 中转下载该仓库最新稳定版本中的固定资产(例如 moesnow/March7thAssistantupdate.7z)。

改动

后端

  • internal/store/sqlite.go:新增 release_sources 表(owner/repo 使用 COLLATE NOCASEowner+repo+asset_name 唯一)与 CRUD 方法;数据存于 /data/devbox.db,重启后保留。顺带把 New() 中手写的路径裁剪改为 filepath.Dir
  • internal/dashboard/release.go(新增):GET/POST /api/release-sourcesDELETE /api/release-sources/{id}POST /api/release-sources/{id}/download-ticketGET /api/release-download?ticket=
  • internal/server/server.go:注册上述路由(另有 4 行为 gofmt 修正既有缩进)。

前端

  • 新增 web/src/views/Releases.vue/releases 路由、导航项:添加表单、来源列表(最新 tag、发布时间、大小、sha256、可用状态)、download / refresh / 行内二次确认 delete。
  • web/src/api/client.ts 新增对应 API 封装。

安全设计

  • URL 只接受 https://github.com/{owner}/{repo}/releases,拒绝其他 host、端口、userinfo、query、fragment;资产名禁止路径分隔符与控制字符。
  • 下载走一次性票据:申请票据需面板 Bearer 鉴权,票据 crypto/rand 生成、2 分钟有效、消费即删除、总量上限 4096,因此面板 token 不会出现在下载 URL 中。
  • 下载时按票据内已校验的 asset ID 拼 api.github.com 地址,不直接代理任意 browser_download_url;重定向仅允许 HTTPS 的 github.com*.githubusercontent.com,并限制跳转次数。
  • 资产以 io.Copy 流式转发,不落盘、不进内存;客户端断开时随请求上下文取消上游下载。
  • Release 元数据按仓库缓存 5 分钟、同仓库多资产只请求一次、限并发 4,避免耗尽未认证 GitHub API 的 60 次/小时额度。
  • 当前仅支持公开仓库,未引入任何 GitHub 凭据。

测试

  • go build ./...go vet ./...go test ./...pnpm run build 全部通过。
  • 新增单测:store CRUD / 重启持久化 / 大小写重复冲突;handler 鉴权、URL 校验、创建+票据+下载全链路、票据重放、重定向白名单。
  • 真实 GitHub 冒烟(本地实例,测试后已清理):添加 moesnow/March7thAssistant 返回 v2026.7.26 / update.7z / 177465850 字节 / sha256 一致;下载响应头 Content-Length: 177465850Content-Disposition: attachment; filename=update.7z,确认流式转发生效;票据重放与伪造票据均 401;非 github.com URL 400、重复源 409、资产不存在 422、删除 200 / 再删 404。

未包含

  • 既有依赖漏洞未在本 PR 处理:govulncheck 报 Go 标准库 GO-2026-5856(需 go1.26.5),pnpm audit 报 axios <1.18.0 与 postcss <=8.5.17。
  • web/pnpm-lock.yaml 因本地 pnpm 重新解析产生的无关改动已还原,未纳入提交。

Summary by CodeRabbit

  • New Features
    • Added a Releases page to manage public GitHub Release sources and pin downloadable assets.
    • Sources persist across restarts; users can add/refresh sources, create secure download tickets, download assets, and delete sources.
    • New dashboard-backed API endpoints enable release source listing/creation and ticket-based asset downloads.
  • Bug Fixes
    • Improved handling of unauthorized registry responses.
    • Fixed Releases view formatting so valid values like 0 no longer appear as missing.
  • Documentation
    • Updated README with “GitHub Release 下载” and /data persistence details.
  • Tests
    • Expanded SQLite CRUD persistence tests and release caching/ticket flow coverage.

Store custom GitHub Releases sources in SQLite so they survive restarts,
then stream the pinned asset of the latest stable release through DevBox
with one click from the new Releases page.

- store: release_sources table (case-insensitive owner/repo, unique per
  owner/repo/asset) with create/list/get/delete helpers
- dashboard: /api/release-sources CRUD, one-time download tickets and a
  streaming /api/release-download endpoint
- dashboard: validate github.com/{owner}/{repo}/releases URLs and asset
  names, cache latest-release lookups for 5m to save GitHub API quota
- dashboard: resolve assets by verified asset ID and restrict download
  redirects to github.com and *.githubusercontent.com
- web: Releases page with add form, source list and download/delete
- tests: store CRUD/persistence plus handler, ticket and redirect tests
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds persistent GitHub Release sources, authenticated APIs, secure ticket-based asset downloads, backend tests, and Releases dashboard integration with navigation, API helpers, and documentation.

Changes

GitHub Release downloads

Layer / File(s) Summary
Release source persistence
internal/store/sqlite.go, internal/store/sqlite_test.go
Adds the release_sources SQLite table and CRUD methods, with uniqueness and reopen persistence tests.
Release API and download logic
internal/dashboard/dashboard.go, internal/dashboard/release.go
Adds GitHub release caching, source validation and management, short-lived download tickets, secure redirects, and streamed asset proxying.
Server wiring and backend validation
internal/server/server.go, internal/dashboard/release_test.go
Registers release endpoints and tests authentication, ticket downloads, replay rejection, cache refresh, URL validation, timeout settings, and redirect restrictions.
Releases dashboard interface
web/src/api/client.ts, web/src/router/index.ts, web/src/App.vue, web/src/views/Releases.vue, web/src/views/Settings.vue
Adds release API helpers, navigation and routing, release-related formatting behavior, and the Release Downloads capability label.
Release capability documentation
README.md
Documents release sources, validation, persistent storage, stable-release downloads, and public repository support.

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

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant Dashboard
  participant GitHub
  participant Store
  Browser->>Dashboard: create release source
  Dashboard->>GitHub: fetch latest release
  Dashboard->>Store: persist validated source
  Browser->>Dashboard: request download ticket
  Dashboard->>GitHub: resolve release asset
  Dashboard-->>Browser: return ticket
  Browser->>Dashboard: download with ticket
  Dashboard->>GitHub: stream release asset
  Dashboard-->>Browser: return file
Loading
🚥 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 clearly and concisely summarizes the main change: persistent GitHub release download sources.
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/release-sources

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

@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: 3

🤖 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/dashboard/release.go`:
- Line 191: Update createDownloadTicket and createReleaseSource to call
latestRelease without unconditionally forcing a refresh, so the existing
five-minute release cache is reused and GitHub is queried only when the cached
asset is missing or stale. Preserve refresh behavior where required for cache
population or invalidation, and ensure normal download requests do not consume
an API call on every invocation.
- Around line 456-469: Update newReleaseDownloadClient to bound connection
establishment and response-header waiting by configuring an http.Transport with
net.Dialer connection timeouts and ResponseHeaderTimeout. Keep the client’s
total Timeout unset so large release downloads are not interrupted, and preserve
the existing redirect validation behavior.

In `@web/src/views/Releases.vue`:
- Around line 115-125: Update formatBytes to treat only undefined or otherwise
absent values as missing, while preserving zero as a valid input that returns “0
B”. Keep the existing unit conversion and formatting for positive byte values
unchanged.
🪄 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: 90626f1d-f360-46d0-947e-aa93c7c2843a

📥 Commits

Reviewing files that changed from the base of the PR and between 344d6c5 and e00537f.

📒 Files selected for processing (12)
  • README.md
  • internal/dashboard/dashboard.go
  • internal/dashboard/release.go
  • internal/dashboard/release_test.go
  • internal/server/server.go
  • internal/store/sqlite.go
  • internal/store/sqlite_test.go
  • web/src/App.vue
  • web/src/api/client.ts
  • web/src/router/index.ts
  • web/src/views/Releases.vue
  • web/src/views/Settings.vue

Comment thread internal/dashboard/release.go Outdated
Comment thread internal/dashboard/release.go
Comment thread web/src/views/Releases.vue
- dashboard: resolve the pinned asset from the cached latest release and
  only re-query GitHub when the cache is cold or no longer lists it, so
  downloads stop spending an API call each time
- dashboard: bound dial, TLS handshake and response-header waits on the
  download client while leaving Client.Timeout unset for large assets
- web: keep a zero asset size rendering as "0 B" instead of "-"
- tests: assert the warm cache serves tickets, stale caches refresh and
  the download client keeps bounded connection timeouts

@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: 1

🤖 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/dashboard/release_test.go`:
- Around line 144-149: Correct TestLatestReleaseAssetRefreshesStaleCache to
match its fixture: either rename it to describe refreshing a cached release
missing the requested asset, or change expiresAt to a time before now and add
assertions verifying TTL-based refresh. Keep the test’s intended behavior and
assertions aligned with the selected scenario.
🪄 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: ea8b75bc-b92b-4504-8b02-7269539758cb

📥 Commits

Reviewing files that changed from the base of the PR and between e00537f and 5d775ec.

📒 Files selected for processing (3)
  • internal/dashboard/release.go
  • internal/dashboard/release_test.go
  • web/src/views/Releases.vue

Comment thread internal/dashboard/release_test.go Outdated
TestLatestReleaseAssetRefreshesStaleCache seeded an unexpired entry, so it
never exercised the TTL path its name promised.

- rename it to TestLatestReleaseAssetRefreshesWhenCachedAssetMissing, which
  is what the fixture actually covers
- add TestLatestReleaseAssetRefreshesExpiredCache for the TTL path: an
  expired entry that still lists the asset must be re-queried and recached

@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: 1

🤖 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/dashboard/release_test.go`:
- Around line 163-165: Strengthen the missing-asset assertion around
dashboard.latestReleaseAsset by also requiring the GitHub mock call count to
equal 2, confirming the missing-asset path refreshes metadata before returning
errAssetNotFound.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f588e4f9-e78b-4b0f-865c-bab5735f9100

📥 Commits

Reviewing files that changed from the base of the PR and between 5d775ec and 27439d2.

📒 Files selected for processing (1)
  • internal/dashboard/release_test.go

Comment on lines +163 to +165
if _, _, err := dashboard.latestReleaseAsset(req, "moesnow", "March7thAssistant", "missing.7z"); !errors.Is(err, errAssetNotFound) {
t.Fatalf("expected errAssetNotFound, got %v", err)
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that the missing-asset path actually refreshes metadata.

This assertion would pass even if the implementation returned errAssetNotFound without re-querying GitHub. Since the test promises refresh behavior, also require calls == 2.

Proposed fix
-	if _, _, err := dashboard.latestReleaseAsset(req, "moesnow", "March7thAssistant", "missing.7z"); !errors.Is(err, errAssetNotFound) {
-		t.Fatalf("expected errAssetNotFound, got %v", err)
+	if _, _, err := dashboard.latestReleaseAsset(req, "moesnow", "March7thAssistant", "missing.7z"); !errors.Is(err, errAssetNotFound) || calls != 2 {
+		t.Fatalf("expected refresh and errAssetNotFound, calls=%d err=%v", calls, err)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if _, _, err := dashboard.latestReleaseAsset(req, "moesnow", "March7thAssistant", "missing.7z"); !errors.Is(err, errAssetNotFound) {
t.Fatalf("expected errAssetNotFound, got %v", err)
}
if _, _, err := dashboard.latestReleaseAsset(req, "moesnow", "March7thAssistant", "missing.7z"); !errors.Is(err, errAssetNotFound) || calls != 2 {
t.Fatalf("expected refresh and errAssetNotFound, calls=%d err=%v", calls, err)
}
🤖 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/dashboard/release_test.go` around lines 163 - 165, Strengthen the
missing-asset assertion around dashboard.latestReleaseAsset by also requiring
the GitHub mock call count to equal 2, confirming the missing-asset path
refreshes metadata before returning errAssetNotFound.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • internal/dashboard/release_test.go

Commit: 15e82c1a43d6f752bc3e19d82e539d9fc1101f0d

The changes have been pushed to the feature/release-sources branch.

Time taken: 2m 58s

Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
@wha7ev9r wha7ev9r closed this Jul 30, 2026
@wha7ev9r
wha7ev9r deleted the feature/release-sources branch July 30, 2026 05:51
wha7ev9r added a commit that referenced this pull request Jul 30, 2026
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.

1 participant