Files
software-station/internal/gitea/client.go
Sudo-Ivan 2f0af4c988 Refactor API and background updater functionality
- Updated the StartBackgroundUpdater function to accept a callback for software list updates, improving flexibility.
- Refactored the API handlers to utilize a proxied software list, enhancing data handling and response efficiency.
- Introduced a new method for refreshing the proxied software list, ensuring accurate data representation.
- Added unit tests for API handlers to validate functionality and response correctness.
2025-12-27 03:30:18 -06:00

293 lines
6.7 KiB
Go

package gitea
import (
"bufio"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"software-station/internal/models"
)
func DetectOS(filename string) string {
lower := strings.ToLower(filename)
osMap := []struct {
patterns []string
suffixes []string
os string
}{
{
patterns: []string{"windows"},
suffixes: []string{".exe", ".msi"},
os: models.OSWindows,
},
{
patterns: []string{"linux"},
suffixes: []string{".deb", ".rpm", ".appimage", ".flatpak"},
os: models.OSLinux,
},
{
patterns: []string{"mac", "darwin"},
suffixes: []string{".dmg", ".pkg"},
os: models.OSMacOS,
},
{
patterns: []string{"freebsd"},
os: models.OSFreeBSD,
},
{
patterns: []string{"openbsd"},
os: models.OSOpenBSD,
},
{
patterns: []string{"android"},
suffixes: []string{".apk"},
os: models.OSAndroid,
},
{
patterns: []string{"arm", "aarch64"},
os: models.OSARM,
},
}
for _, entry := range osMap {
for _, p := range entry.patterns {
if strings.Contains(lower, p) {
return entry.os
}
}
for _, s := range entry.suffixes {
if strings.HasSuffix(lower, s) {
return entry.os
}
}
}
return models.OSUnknown
}
func FetchRepoInfo(server, token, owner, repo string) (string, []string, string, bool, string, error) {
url := fmt.Sprintf("%s%s/%s/%s", server, RepoAPIPath, owner, repo)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", nil, "", false, "", err
}
if token != "" {
req.Header.Set("Authorization", "token "+token)
}
client := &http.Client{Timeout: DefaultTimeout}
resp, err := client.Do(req)
if err != nil {
return "", nil, "", false, "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", nil, "", false, "", fmt.Errorf("gitea api returned status %d", resp.StatusCode)
}
var info struct {
Description string `json:"description"`
Topics []string `json:"topics"`
DefaultBranch string `json:"default_branch"`
Licenses []string `json:"licenses"`
Private bool `json:"private"`
AvatarURL string `json:"avatar_url"`
}
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return "", nil, "", false, "", err
}
license := ""
if len(info.Licenses) > 0 {
license = info.Licenses[0]
}
if license == "" {
// Try to detect license from file if API returns nothing
license = detectLicenseFromFile(server, token, owner, repo, info.DefaultBranch)
}
return info.Description, info.Topics, license, info.Private, info.AvatarURL, nil
}
func detectLicenseFromFile(server, token, owner, repo, defaultBranch string) string {
branches := []string{"main", "master"}
if defaultBranch != "" {
branches = append([]string{defaultBranch}, "main", "master")
}
seen := make(map[string]bool)
var finalBranches []string
for _, b := range branches {
if !seen[b] {
seen[b] = true
finalBranches = append(finalBranches, b)
}
}
for _, branch := range finalBranches {
url := fmt.Sprintf("%s/%s/%s/raw/branch/%s/LICENSE", server, owner, repo, branch)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
continue
}
if token != "" {
req.Header.Set("Authorization", "token "+token)
}
client := &http.Client{Timeout: DefaultTimeout}
resp, err := client.Do(req)
if err != nil {
continue
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
scanner := bufio.NewScanner(resp.Body)
for i := 0; i < 5 && scanner.Scan(); i++ {
line := strings.ToUpper(scanner.Text())
if strings.Contains(line, "MIT LICENSE") {
return "MIT"
}
if strings.Contains(line, "GNU GENERAL PUBLIC LICENSE") || strings.Contains(line, "GPL") {
return "GPL"
}
if strings.Contains(line, "APACHE LICENSE") {
return "Apache-2.0"
}
if strings.Contains(line, "BSD") {
return "BSD"
}
}
return "LICENSE"
}
}
return ""
}
func IsSBOM(filename string) bool {
lower := strings.ToLower(filename)
return strings.Contains(lower, "sbom") ||
strings.Contains(lower, "cyclonedx") ||
strings.Contains(lower, "spdx") ||
strings.HasSuffix(lower, ".cdx.json") ||
strings.HasSuffix(lower, ".spdx.json")
}
func FetchReleases(server, token, owner, repo string) ([]models.Release, error) {
url := fmt.Sprintf("%s%s/%s/%s%s", server, RepoAPIPath, owner, repo, ReleasesSuffix)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
if token != "" {
req.Header.Set("Authorization", "token "+token)
}
client := &http.Client{Timeout: DefaultTimeout}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("gitea api returned status %d", resp.StatusCode)
}
var giteaReleases []struct {
TagName string `json:"tag_name"`
Body string `json:"body"`
CreatedAt time.Time `json:"created_at"`
Assets []struct {
Name string `json:"name"`
Size int64 `json:"size"`
URL string `json:"browser_download_url"`
} `json:"assets"`
}
if err := json.NewDecoder(resp.Body).Decode(&giteaReleases); err != nil {
return nil, err
}
var releases []models.Release
for _, gr := range giteaReleases {
var assets []models.Asset
var checksumsURL string
for _, ga := range gr.Assets {
if ga.Name == "SHA256SUMS" {
checksumsURL = ga.URL
continue
}
assets = append(assets, models.Asset{
Name: ga.Name,
Size: ga.Size,
URL: ga.URL,
OS: DetectOS(ga.Name),
IsSBOM: IsSBOM(ga.Name),
})
}
if checksumsURL != "" {
checksums, err := fetchAndParseChecksums(checksumsURL, token)
if err == nil {
for i := range assets {
if sha, ok := checksums[assets[i].Name]; ok {
assets[i].SHA256 = sha
}
}
}
}
releases = append(releases, models.Release{
TagName: gr.TagName,
Body: gr.Body,
CreatedAt: gr.CreatedAt,
Assets: assets,
})
}
return releases, nil
}
func fetchAndParseChecksums(url, token string) (map[string]string, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
if token != "" {
req.Header.Set("Authorization", "token "+token)
}
client := &http.Client{Timeout: DefaultTimeout}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch checksums: %d", resp.StatusCode)
}
checksums := make(map[string]string)
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
parts := strings.Fields(line)
if len(parts) >= 2 {
checksums[parts[1]] = parts[0]
}
}
return checksums, nil
}