299 lines
7.0 KiB
Go
299 lines
7.0 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 != "" {
|
|
// Put default branch first
|
|
branches = append([]string{defaultBranch}, "main", "master")
|
|
}
|
|
// Deduplicate
|
|
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 {
|
|
// Read first few lines to guess license
|
|
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" // Found file but couldn't detect type
|
|
}
|
|
}
|
|
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
|
|
|
|
// First pass: identify assets and look for checksum file
|
|
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),
|
|
})
|
|
}
|
|
|
|
// Second pass: if checksum file exists, fetch and parse it
|
|
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 {
|
|
// Format is usually: hash filename
|
|
checksums[parts[1]] = parts[0]
|
|
}
|
|
}
|
|
return checksums, nil
|
|
}
|