143 lines
3.7 KiB
Go
143 lines
3.7 KiB
Go
package rpm
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"context"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type NewPackage struct {
|
|
Name string
|
|
Version string
|
|
Arch string
|
|
Location string
|
|
Checksum string
|
|
ChecksumType string
|
|
Size int64
|
|
}
|
|
|
|
type Scanner struct {
|
|
SourceURL string
|
|
LocalDir string
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
func NewScanner(sourceURL, localDir string) *Scanner {
|
|
return &Scanner{
|
|
SourceURL: strings.TrimRight(sourceURL, "/"),
|
|
LocalDir: localDir,
|
|
HTTPClient: &http.Client{Timeout: 5 * time.Minute},
|
|
}
|
|
}
|
|
|
|
// Scan fetches the remote package list and returns packages not present on disk.
|
|
func (sc *Scanner) Scan(ctx context.Context) ([]NewPackage, error) {
|
|
repomd, err := sc.fetchRepoMD(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetch repomd.xml: %w", err)
|
|
}
|
|
|
|
var primaryEntry *RepoMDEntry
|
|
for i := range repomd.Data {
|
|
if repomd.Data[i].Type == "primary" {
|
|
primaryEntry = &repomd.Data[i]
|
|
break
|
|
}
|
|
}
|
|
if primaryEntry == nil {
|
|
return nil, fmt.Errorf("no primary entry in repomd.xml")
|
|
}
|
|
|
|
packages, err := sc.fetchPrimary(ctx, *primaryEntry)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetch primary.xml: %w", err)
|
|
}
|
|
|
|
var missing []NewPackage
|
|
for _, pkg := range packages {
|
|
localPath := filepath.Join(sc.LocalDir, filepath.FromSlash(pkg.Location.Href))
|
|
if _, err := os.Stat(localPath); os.IsNotExist(err) {
|
|
ver := pkg.Version.Ver + "-" + pkg.Version.Rel
|
|
if pkg.Version.Epoch != "0" && pkg.Version.Epoch != "" {
|
|
ver = pkg.Version.Epoch + ":" + ver
|
|
}
|
|
missing = append(missing, NewPackage{
|
|
Name: pkg.Name,
|
|
Version: ver,
|
|
Arch: pkg.Arch,
|
|
Location: pkg.Location.Href,
|
|
Checksum: pkg.Checksum.Value,
|
|
ChecksumType: pkg.Checksum.Type,
|
|
Size: pkg.Size.Package,
|
|
})
|
|
}
|
|
}
|
|
return missing, nil
|
|
}
|
|
|
|
func (sc *Scanner) fetchRepoMD(ctx context.Context) (*RepoMD, error) {
|
|
data, err := sc.fetchBytes(ctx, sc.SourceURL+"/repodata/repomd.xml")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var repomd RepoMD
|
|
if err := xml.Unmarshal(data, &repomd); err != nil {
|
|
return nil, fmt.Errorf("parse repomd.xml: %w", err)
|
|
}
|
|
return &repomd, nil
|
|
}
|
|
|
|
func (sc *Scanner) fetchPrimary(ctx context.Context, entry RepoMDEntry) ([]Package, error) {
|
|
data, err := sc.fetchBytes(ctx, sc.SourceURL+"/"+entry.Location.Href)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if entry.Checksum.Type == "sha256" {
|
|
if err := verifyChecksum(data, entry.Checksum.Value); err != nil {
|
|
return nil, fmt.Errorf("primary.xml.gz: %w", err)
|
|
}
|
|
}
|
|
|
|
// Save upstream primary to disk so RegenerateMetadata can use it as source.
|
|
dest := filepath.Join(sc.LocalDir, filepath.FromSlash(entry.Location.Href))
|
|
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err == nil {
|
|
if err := writeFile(dest, data); err == nil {
|
|
marker := filepath.Join(sc.LocalDir, "repodata", PrimarySourceFile)
|
|
_ = os.WriteFile(marker, []byte(entry.Location.Href), 0o644)
|
|
}
|
|
}
|
|
|
|
gz, err := gzip.NewReader(strings.NewReader(string(data)))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open gzip: %w", err)
|
|
}
|
|
defer gz.Close()
|
|
var primary PrimaryMetadata
|
|
if err := xml.NewDecoder(gz).Decode(&primary); err != nil {
|
|
return nil, fmt.Errorf("parse primary.xml: %w", err)
|
|
}
|
|
return primary.Packages, nil
|
|
}
|
|
|
|
func (sc *Scanner) fetchBytes(ctx context.Context, url string) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := sc.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("HTTP %d for %s", resp.StatusCode, url)
|
|
}
|
|
return io.ReadAll(resp.Body)
|
|
}
|