195 lines
4.7 KiB
Go
195 lines
4.7 KiB
Go
package rpm
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"context"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Progress struct {
|
|
File string
|
|
BytesDone int64
|
|
}
|
|
|
|
type ProgressFunc func(p Progress)
|
|
|
|
type Cloner struct {
|
|
SourceURL string
|
|
DestDir string
|
|
HTTPClient *http.Client
|
|
OnProgress ProgressFunc
|
|
}
|
|
|
|
func New(sourceURL, destDir string) *Cloner {
|
|
return &Cloner{
|
|
SourceURL: strings.TrimRight(sourceURL, "/"),
|
|
DestDir: destDir,
|
|
HTTPClient: &http.Client{Timeout: 30 * time.Minute},
|
|
}
|
|
}
|
|
|
|
func (c *Cloner) Clone(ctx context.Context) error {
|
|
if err := os.MkdirAll(filepath.Join(c.DestDir, "repodata"), 0o755); err != nil {
|
|
return fmt.Errorf("create repodata dir: %w", err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Join(c.DestDir, "Packages"), 0o755); err != nil {
|
|
return fmt.Errorf("create Packages dir: %w", err)
|
|
}
|
|
|
|
repomd, err := c.fetchRepoMD(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("fetch repomd.xml: %w", err)
|
|
}
|
|
|
|
var primaryEntry *RepoMDEntry
|
|
for i, entry := range repomd.Data {
|
|
if entry.Type == "primary" {
|
|
primaryEntry = &repomd.Data[i]
|
|
continue
|
|
}
|
|
if err := c.downloadMetadataFile(ctx, entry); err != nil {
|
|
return fmt.Errorf("download metadata %s: %w", entry.Type, err)
|
|
}
|
|
}
|
|
|
|
if primaryEntry == nil {
|
|
return fmt.Errorf("no primary metadata found in repomd.xml")
|
|
}
|
|
|
|
packages, err := c.fetchPrimary(ctx, *primaryEntry)
|
|
if err != nil {
|
|
return fmt.Errorf("fetch primary.xml: %w", err)
|
|
}
|
|
|
|
for _, pkg := range packages {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if err := c.downloadPackage(ctx, pkg); err != nil {
|
|
return fmt.Errorf("download package %s: %w", pkg.Name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Cloner) fetchRepoMD(ctx context.Context) (*RepoMD, error) {
|
|
url := c.SourceURL + "/repodata/repomd.xml"
|
|
data, err := c.fetchBytes(ctx, url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dest := filepath.Join(c.DestDir, "repodata", "repomd.xml")
|
|
if err := writeFile(dest, data); 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 (c *Cloner) fetchPrimary(ctx context.Context, entry RepoMDEntry) ([]Package, error) {
|
|
url := c.SourceURL + "/" + entry.Location.Href
|
|
data, err := c.fetchBytes(ctx, url)
|
|
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)
|
|
}
|
|
}
|
|
|
|
dest := filepath.Join(c.DestDir, entry.Location.Href)
|
|
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := writeFile(dest, data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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 (c *Cloner) downloadMetadataFile(ctx context.Context, entry RepoMDEntry) error {
|
|
url := c.SourceURL + "/" + entry.Location.Href
|
|
dest := filepath.Join(c.DestDir, entry.Location.Href)
|
|
|
|
n, err := c.downloadAndVerify(ctx, url, dest, entry.Checksum.Type, entry.Checksum.Value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if c.OnProgress != nil {
|
|
c.OnProgress(Progress{File: entry.Location.Href, BytesDone: n})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Cloner) downloadPackage(ctx context.Context, pkg Package) error {
|
|
url := c.SourceURL + "/" + pkg.Location.Href
|
|
dest := filepath.Join(c.DestDir, pkg.Location.Href)
|
|
|
|
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
|
return err
|
|
}
|
|
|
|
n, err := c.downloadAndVerify(ctx, url, dest, pkg.Checksum.Type, pkg.Checksum.Value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if c.OnProgress != nil {
|
|
c.OnProgress(Progress{File: pkg.Location.Href, BytesDone: n})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Cloner) downloadAndVerify(ctx context.Context, url, destPath, checksumType, expectedChecksum string) (int64, error) {
|
|
return DownloadAndVerify(ctx, c.HTTPClient, url, destPath, checksumType, expectedChecksum)
|
|
}
|
|
|
|
func (c *Cloner) 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 := c.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)
|
|
}
|
|
|
|
func writeFile(path string, data []byte) error {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, data, 0o644)
|
|
}
|
|
|