- 基于官方最新代码 upstream/main (93db889)
- 包含所有官方新功能:运维监控、促销码系统、Linux DO OAuth 等
- 仅修改品牌显示名称,保持所有功能完整
- 修改范围:前后端界面文字、i18n、邮件模板、日志输出
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
137 lines
3.5 KiB
Go
137 lines
3.5 KiB
Go
package repository
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"os"
|
||
"time"
|
||
|
||
"github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
|
||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||
)
|
||
|
||
type githubReleaseClient struct {
|
||
httpClient *http.Client
|
||
downloadHTTPClient *http.Client
|
||
}
|
||
|
||
// NewGitHubReleaseClient 创建 GitHub Release 客户端
|
||
// proxyURL 为空时直连 GitHub,支持 http/https/socks5/socks5h 协议
|
||
func NewGitHubReleaseClient(proxyURL string) service.GitHubReleaseClient {
|
||
sharedClient, err := httpclient.GetClient(httpclient.Options{
|
||
Timeout: 30 * time.Second,
|
||
ProxyURL: proxyURL,
|
||
})
|
||
if err != nil {
|
||
sharedClient = &http.Client{Timeout: 30 * time.Second}
|
||
}
|
||
|
||
// 下载客户端需要更长的超时时间
|
||
downloadClient, err := httpclient.GetClient(httpclient.Options{
|
||
Timeout: 10 * time.Minute,
|
||
ProxyURL: proxyURL,
|
||
})
|
||
if err != nil {
|
||
downloadClient = &http.Client{Timeout: 10 * time.Minute}
|
||
}
|
||
|
||
return &githubReleaseClient{
|
||
httpClient: sharedClient,
|
||
downloadHTTPClient: downloadClient,
|
||
}
|
||
}
|
||
|
||
func (c *githubReleaseClient) FetchLatestRelease(ctx context.Context, repo string) (*service.GitHubRelease, error) {
|
||
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repo)
|
||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||
req.Header.Set("User-Agent", "TianShuAPI-Updater")
|
||
|
||
resp, err := c.httpClient.Do(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer func() { _ = resp.Body.Close() }()
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
return nil, fmt.Errorf("GitHub API returned %d", resp.StatusCode)
|
||
}
|
||
|
||
var release service.GitHubRelease
|
||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &release, nil
|
||
}
|
||
|
||
func (c *githubReleaseClient) DownloadFile(ctx context.Context, url, dest string, maxSize int64) error {
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 使用预配置的下载客户端(已包含代理配置)
|
||
resp, err := c.downloadHTTPClient.Do(req)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer func() { _ = resp.Body.Close() }()
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
return fmt.Errorf("download returned %d", resp.StatusCode)
|
||
}
|
||
|
||
// SECURITY: Check Content-Length if available
|
||
if resp.ContentLength > maxSize {
|
||
return fmt.Errorf("file too large: %d bytes (max %d)", resp.ContentLength, maxSize)
|
||
}
|
||
|
||
out, err := os.Create(dest)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer func() { _ = out.Close() }()
|
||
|
||
// SECURITY: Use LimitReader to enforce max download size even if Content-Length is missing/wrong
|
||
limited := io.LimitReader(resp.Body, maxSize+1)
|
||
written, err := io.Copy(out, limited)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Check if we hit the limit (downloaded more than maxSize)
|
||
if written > maxSize {
|
||
_ = os.Remove(dest) // Clean up partial file (best-effort)
|
||
return fmt.Errorf("download exceeded maximum size of %d bytes", maxSize)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (c *githubReleaseClient) FetchChecksumFile(ctx context.Context, url string) ([]byte, error) {
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
resp, err := c.httpClient.Do(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer func() { _ = resp.Body.Close() }()
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
|
||
}
|
||
|
||
return io.ReadAll(resp.Body)
|
||
}
|