| package repository |
|
|
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "io" |
| "log/slog" |
| "net/http" |
| "os" |
| "strings" |
| "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 |
| } |
|
|
| type githubReleaseClientError struct { |
| err error |
| } |
|
|
| |
| |
| |
| |
| |
| func NewGitHubReleaseClient(proxyURL string, allowDirectOnProxyError bool) service.GitHubReleaseClient { |
| |
| |
| sharedClient, err := httpclient.GetClient(httpclient.Options{ |
| Timeout: 30 * time.Second, |
| ProxyURL: proxyURL, |
| }) |
| if err != nil { |
| if strings.TrimSpace(proxyURL) != "" && !allowDirectOnProxyError { |
| slog.Warn("proxy client init failed, all requests will fail", "service", "github_release", "error", err) |
| return &githubReleaseClientError{err: fmt.Errorf("proxy client init failed and direct fallback is disabled; set security.proxy_fallback.allow_direct_on_error=true to allow fallback: %w", err)} |
| } |
| sharedClient = &http.Client{Timeout: 30 * time.Second} |
| } |
|
|
| |
| downloadClient, err := httpclient.GetClient(httpclient.Options{ |
| Timeout: 10 * time.Minute, |
| ProxyURL: proxyURL, |
| }) |
| if err != nil { |
| if strings.TrimSpace(proxyURL) != "" && !allowDirectOnProxyError { |
| slog.Warn("proxy download client init failed, all requests will fail", "service", "github_release", "error", err) |
| return &githubReleaseClientError{err: fmt.Errorf("proxy client init failed and direct fallback is disabled; set security.proxy_fallback.allow_direct_on_error=true to allow fallback: %w", err)} |
| } |
| downloadClient = &http.Client{Timeout: 10 * time.Minute} |
| } |
|
|
| return &githubReleaseClient{ |
| httpClient: sharedClient, |
| downloadHTTPClient: downloadClient, |
| } |
| } |
|
|
| func (c *githubReleaseClientError) FetchLatestRelease(ctx context.Context, repo string) (*service.GitHubRelease, error) { |
| return nil, c.err |
| } |
|
|
| func (c *githubReleaseClientError) DownloadFile(ctx context.Context, url, dest string, maxSize int64) error { |
| return c.err |
| } |
|
|
| func (c *githubReleaseClientError) FetchChecksumFile(ctx context.Context, url string) ([]byte, error) { |
| return nil, c.err |
| } |
|
|
| 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", "Sub2API-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) |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| limited := io.LimitReader(resp.Body, maxSize+1) |
| written, err := io.Copy(out, limited) |
|
|
| |
| _ = out.Close() |
|
|
| if err != nil { |
| _ = os.Remove(dest) |
| return err |
| } |
|
|
| |
| if written > maxSize { |
| _ = os.Remove(dest) |
| 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) |
| } |
|
|