mirror of
https://github.com/webinstall/webi-installers.git
synced 2026-05-18 14:46:37 +00:00
Replaces the inline &http.Client{Timeout: 30s} with httpclient.New(),
which enforces TLS 1.2+, per-level timeouts, no HTTPS→HTTP redirect
downgrade, connection pooling, and automatic retry with backoff.
The delayTransport (page-delay flag) now wraps httpclient's transport
instead of http.DefaultTransport, preserving all security properties.
926 lines
25 KiB
Go
926 lines
25 KiB
Go
// Command webicached is the release cache daemon. It fetches releases
|
|
// from upstream sources, classifies build assets, and writes them to
|
|
// the _cache/ directory in the format the Node.js server expects.
|
|
//
|
|
// This is the Go replacement for the Node.js release-fetching pipeline.
|
|
// It reads releases.conf files to discover packages, fetches from the
|
|
// configured source, classifies assets, and writes to fsstore.
|
|
//
|
|
// Default mode: classify all from existing rawcache on startup, then
|
|
// fetch+refresh one package per tick (round-robin, 15m default).
|
|
//
|
|
// Usage:
|
|
//
|
|
// go run ./cmd/webicached # default: round-robin, one per tick
|
|
// go run ./cmd/webicached -eager # fetch all packages on startup
|
|
// go run ./cmd/webicached -once -no-fetch # classify from rawcache and exit
|
|
// go run ./cmd/webicached bat goreleaser # only these packages
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math/rand/v2"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/joho/godotenv"
|
|
"github.com/webinstall/webi-installers/internal/classifypkg"
|
|
"github.com/webinstall/webi-installers/internal/httpclient"
|
|
"github.com/webinstall/webi-installers/internal/installerconf"
|
|
"github.com/webinstall/webi-installers/internal/rawcache"
|
|
"github.com/webinstall/webi-installers/internal/releases/chromedist"
|
|
"github.com/webinstall/webi-installers/internal/releases/flutterdist"
|
|
"github.com/webinstall/webi-installers/internal/releases/gitea"
|
|
"github.com/webinstall/webi-installers/internal/releases/github"
|
|
"github.com/webinstall/webi-installers/internal/releases/githubish"
|
|
"github.com/webinstall/webi-installers/internal/releases/gittag"
|
|
"github.com/webinstall/webi-installers/internal/releases/golang"
|
|
"github.com/webinstall/webi-installers/internal/releases/gpgdist"
|
|
"github.com/webinstall/webi-installers/internal/releases/hashicorp"
|
|
"github.com/webinstall/webi-installers/internal/releases/iterm2dist"
|
|
"github.com/webinstall/webi-installers/internal/releases/juliadist"
|
|
"github.com/webinstall/webi-installers/internal/releases/mariadbdist"
|
|
"github.com/webinstall/webi-installers/internal/releases/nodedist"
|
|
"github.com/webinstall/webi-installers/internal/releases/servicemandist"
|
|
"github.com/webinstall/webi-installers/internal/releases/zigdist"
|
|
"github.com/webinstall/webi-installers/internal/storage"
|
|
"github.com/webinstall/webi-installers/internal/storage/fsstore"
|
|
)
|
|
|
|
var (
|
|
name = "webicached"
|
|
version = "0.0.0-dev"
|
|
commit = "0000000"
|
|
date = "0001-01-01"
|
|
licenseYear = "2024"
|
|
licenseOwner = "AJ ONeal"
|
|
licenseType = "MPL-2.0"
|
|
)
|
|
|
|
func printVersion(w io.Writer) {
|
|
b_ver := strings.TrimPrefix(version, "v")
|
|
_, _ = fmt.Fprintf(w, "%s v%s %s (%s)\n", name, b_ver, commit[:7], date)
|
|
_, _ = fmt.Fprintf(w, "Copyright (C) %s %s\n", licenseYear, licenseOwner)
|
|
_, _ = fmt.Fprintf(w, "Licensed under %s\n", licenseType)
|
|
}
|
|
|
|
type MainConfig struct {
|
|
envFile string
|
|
confDir string
|
|
cacheDir string
|
|
rawDir string
|
|
token string
|
|
once bool
|
|
noFetch bool
|
|
shallow bool
|
|
eager bool
|
|
interval time.Duration
|
|
pageDelay time.Duration
|
|
}
|
|
|
|
// WebiCache holds the configuration for the cache daemon.
|
|
type WebiCache struct {
|
|
ConfDir string // root directory with {pkg}/releases.conf files
|
|
Store storage.Store // classified asset storage (fsstore)
|
|
RawDir string // raw upstream response cache
|
|
Client *http.Client // HTTP client for upstream calls
|
|
Auth *githubish.Auth // GitHub API auth (optional)
|
|
Shallow bool // fetch only the first page of releases
|
|
NoFetch bool // skip fetching, classify from existing raw data only
|
|
PageDelay time.Duration // delay between paginated API requests
|
|
}
|
|
|
|
// delayTransport wraps an http.RoundTripper to add a delay between requests.
|
|
type delayTransport struct {
|
|
base http.RoundTripper
|
|
delay time.Duration
|
|
last time.Time
|
|
}
|
|
|
|
func (t *delayTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
if !t.last.IsZero() && t.delay > 0 {
|
|
if wait := t.delay - time.Since(t.last); wait > 0 {
|
|
time.Sleep(wait)
|
|
}
|
|
}
|
|
t.last = time.Now()
|
|
return t.base.RoundTrip(req)
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) > 1 {
|
|
switch os.Args[1] {
|
|
case "-V", "-version", "--version", "version":
|
|
printVersion(os.Stdout)
|
|
os.Exit(0)
|
|
case "help", "-help", "--help":
|
|
printVersion(os.Stdout)
|
|
fmt.Fprintln(os.Stdout, "")
|
|
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
|
fs.SetOutput(os.Stdout)
|
|
registerFlags(fs, &MainConfig{})
|
|
fs.Usage()
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
cfg := MainConfig{}
|
|
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
|
registerFlags(fs, &cfg)
|
|
if err := fs.Parse(os.Args[1:]); err != nil {
|
|
if errors.Is(err, flag.ErrHelp) {
|
|
os.Exit(0)
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
|
|
cfg.cacheDir = expandHome(cfg.cacheDir)
|
|
cfg.rawDir = expandHome(cfg.rawDir)
|
|
|
|
if cfg.envFile != "" {
|
|
if err := godotenv.Load(cfg.envFile); err != nil {
|
|
log.Fatalf("envfile: %v", err)
|
|
}
|
|
}
|
|
if cfg.token == "" {
|
|
cfg.token = os.Getenv("GITHUB_TOKEN")
|
|
}
|
|
|
|
fss, err := fsstore.New(cfg.cacheDir)
|
|
if err != nil {
|
|
log.Fatalf("fsstore: %v", err)
|
|
}
|
|
var store storage.Store = fss
|
|
|
|
var auth *githubish.Auth
|
|
if cfg.token != "" {
|
|
auth = &githubish.Auth{Token: cfg.token}
|
|
}
|
|
|
|
client := httpclient.New()
|
|
if cfg.pageDelay > 0 {
|
|
client.Transport = &delayTransport{
|
|
base: client.Transport,
|
|
delay: cfg.pageDelay,
|
|
}
|
|
}
|
|
|
|
wc := &WebiCache{
|
|
ConfDir: cfg.confDir,
|
|
Store: store,
|
|
RawDir: cfg.rawDir,
|
|
Client: client,
|
|
Auth: auth,
|
|
Shallow: cfg.shallow,
|
|
NoFetch: cfg.noFetch,
|
|
PageDelay: cfg.pageDelay,
|
|
}
|
|
|
|
filterPkgs := fs.Args()
|
|
|
|
if cfg.eager {
|
|
wc.Run(filterPkgs)
|
|
if cfg.once {
|
|
return
|
|
}
|
|
} else if cfg.once {
|
|
wc.Run(filterPkgs)
|
|
return
|
|
} else {
|
|
saved := wc.NoFetch
|
|
wc.NoFetch = true
|
|
wc.Run(filterPkgs)
|
|
wc.NoFetch = saved
|
|
}
|
|
|
|
packages, err := discover(wc.ConfDir)
|
|
if err != nil {
|
|
log.Fatalf("discover: %v", err)
|
|
}
|
|
nameSet := make(map[string]bool, len(filterPkgs))
|
|
for _, a := range filterPkgs {
|
|
nameSet[a] = true
|
|
}
|
|
if len(filterPkgs) > 0 {
|
|
var filtered []pkgConf
|
|
for _, p := range packages {
|
|
if nameSet[p.name] {
|
|
filtered = append(filtered, p)
|
|
}
|
|
}
|
|
packages = filtered
|
|
}
|
|
|
|
var real []pkgConf
|
|
for _, pkg := range packages {
|
|
if pkg.conf.AliasOf == "" {
|
|
real = append(real, pkg)
|
|
}
|
|
}
|
|
|
|
// rescanNew appends any conf files added since the last scan.
|
|
// Returns true when at least one new package was added so the caller
|
|
// can restart the batch loop and process new packages immediately.
|
|
rescanNew := func() bool {
|
|
discovered, err := discover(wc.ConfDir)
|
|
if err != nil {
|
|
log.Printf("rescan: %v", err)
|
|
return false
|
|
}
|
|
known := make(map[string]bool, len(real))
|
|
for _, p := range real {
|
|
known[p.name] = true
|
|
}
|
|
added := false
|
|
for _, p := range discovered {
|
|
if p.conf.AliasOf != "" || known[p.name] {
|
|
continue
|
|
}
|
|
if len(filterPkgs) > 0 && !nameSet[p.name] {
|
|
continue
|
|
}
|
|
log.Printf("discovered new package: %s (source=%s)", p.name, p.conf.Source)
|
|
real = append(real, p)
|
|
added = true
|
|
}
|
|
return added
|
|
}
|
|
|
|
log.Printf("refreshing %d packages, interval %s, batch size 20 (ctrl-c to stop)", len(real), cfg.interval)
|
|
for {
|
|
// Rescan before computing staleness so newly added conf files are
|
|
// included immediately. New packages have a zero timestamp and sort
|
|
// to the front of the stale list, so they are processed next.
|
|
rescanNew()
|
|
|
|
stale := wc.stalest(real)
|
|
if len(stale) == 0 {
|
|
log.Printf("all packages fresh, sleeping %s", cfg.interval)
|
|
time.Sleep(cfg.interval)
|
|
continue
|
|
}
|
|
|
|
batch := stale
|
|
if len(batch) > 20 {
|
|
batch = batch[:20]
|
|
}
|
|
rand.Shuffle(len(batch), func(i, j int) {
|
|
batch[i], batch[j] = batch[j], batch[i]
|
|
})
|
|
|
|
log.Printf("batch: %d stale, refreshing %d (most stale first)", len(stale), len(batch))
|
|
for _, pkg := range batch {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
if err := wc.refreshPackage(ctx, pkg); err != nil {
|
|
log.Printf(" ERROR %s: %v", pkg.name, err)
|
|
}
|
|
cancel()
|
|
time.Sleep(cfg.interval)
|
|
// Rescan mid-batch so new packages preempt remaining batch items.
|
|
if rescanNew() {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func registerFlags(fs *flag.FlagSet, cfg *MainConfig) {
|
|
fs.StringVar(&cfg.envFile, "envfile", "", "path to .env file to load before running")
|
|
fs.StringVar(&cfg.confDir, "conf", ".", "root directory containing {pkg}/releases.conf files")
|
|
fs.StringVar(&cfg.cacheDir, "legacy", "~/.cache/webi/legacy", "legacy cache directory (fsstore root)")
|
|
fs.StringVar(&cfg.rawDir, "raw", "~/.cache/webi/raw", "raw cache directory for upstream responses")
|
|
fs.StringVar(&cfg.token, "token", "", "GitHub API token (or set $GITHUB_TOKEN)")
|
|
fs.BoolVar(&cfg.once, "once", false, "run once then exit (no periodic refresh)")
|
|
fs.BoolVar(&cfg.noFetch, "no-fetch", false, "skip fetching, classify from existing raw data only")
|
|
fs.BoolVar(&cfg.shallow, "shallow", false, "fetch only the first page of releases (latest)")
|
|
fs.BoolVar(&cfg.eager, "eager", false, "fetch all packages on startup (default: one per tick)")
|
|
fs.DurationVar(&cfg.interval, "interval", 9*time.Second, "delay between individual package fetches")
|
|
fs.DurationVar(&cfg.pageDelay, "page-delay", 2*time.Second, "delay between paginated API requests")
|
|
}
|
|
|
|
func expandHome(path string) string {
|
|
if !strings.HasPrefix(path, "~/") {
|
|
return path
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return path
|
|
}
|
|
return filepath.Join(home, path[2:])
|
|
}
|
|
|
|
// stalest returns packages sorted by most stale first (oldest UpdatedAt).
|
|
// Packages with no cache entry or empty assets are considered most stale.
|
|
func (wc *WebiCache) stalest(packages []pkgConf) []pkgConf {
|
|
type stamped struct {
|
|
pkg pkgConf
|
|
updatedAt time.Time
|
|
}
|
|
|
|
var stale []stamped
|
|
ctx := context.Background()
|
|
for _, pkg := range packages {
|
|
data, err := wc.Store.Load(ctx, pkg.name)
|
|
var t time.Time
|
|
hasAssets := false
|
|
if err == nil && data != nil {
|
|
t = data.UpdatedAt
|
|
hasAssets = len(data.Assets) > 0
|
|
}
|
|
// Never fetched, or has no assets despite having a timestamp
|
|
// (e.g. classified from empty rawcache), or older than 10 minutes.
|
|
if t.IsZero() || !hasAssets || time.Since(t) > 10*time.Minute {
|
|
stale = append(stale, stamped{pkg: pkg, updatedAt: t})
|
|
}
|
|
}
|
|
|
|
sort.SliceStable(stale, func(i, j int) bool {
|
|
ti, tj := stale[i].updatedAt, stale[j].updatedAt
|
|
if ti.Equal(tj) {
|
|
return stale[i].pkg.name < stale[j].pkg.name
|
|
}
|
|
return ti.Before(tj)
|
|
})
|
|
|
|
result := make([]pkgConf, len(stale))
|
|
for i, s := range stale {
|
|
result[i] = s.pkg
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Run discovers packages and refreshes each one.
|
|
func (wc *WebiCache) Run(filterPkgs []string) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
|
defer cancel()
|
|
|
|
packages, err := discover(wc.ConfDir)
|
|
if err != nil {
|
|
log.Printf("discover: %v", err)
|
|
return
|
|
}
|
|
|
|
if len(filterPkgs) > 0 {
|
|
nameSet := make(map[string]bool, len(filterPkgs))
|
|
for _, a := range filterPkgs {
|
|
nameSet[a] = true
|
|
}
|
|
var filtered []pkgConf
|
|
for _, p := range packages {
|
|
if nameSet[p.name] {
|
|
filtered = append(filtered, p)
|
|
}
|
|
}
|
|
packages = filtered
|
|
}
|
|
|
|
var real []pkgConf
|
|
for _, pkg := range packages {
|
|
if pkg.conf.AliasOf != "" {
|
|
continue
|
|
}
|
|
real = append(real, pkg)
|
|
}
|
|
|
|
log.Printf("refreshing %d packages", len(real))
|
|
runStart := time.Now()
|
|
|
|
for _, pkg := range real {
|
|
if err := wc.refreshPackage(ctx, pkg); err != nil {
|
|
log.Printf(" ERROR %s: %v", pkg.name, err)
|
|
}
|
|
}
|
|
|
|
log.Printf("refreshed %d packages in %s", len(real), time.Since(runStart))
|
|
}
|
|
|
|
type pkgConf struct {
|
|
name string
|
|
conf *installerconf.Conf
|
|
}
|
|
|
|
func discover(dir string) ([]pkgConf, error) {
|
|
pattern := filepath.Join(dir, "*", "releases.conf")
|
|
matches, err := filepath.Glob(pattern)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var packages []pkgConf
|
|
for _, path := range matches {
|
|
pkgDir := filepath.Dir(path)
|
|
name := filepath.Base(pkgDir)
|
|
if strings.HasPrefix(name, "_") {
|
|
continue
|
|
}
|
|
|
|
// If the package directory is a symlink, treat it as an alias
|
|
// of the symlink target (e.g. rust.vim → vim-rust).
|
|
fi, err := os.Lstat(filepath.Join(dir, name))
|
|
if err != nil {
|
|
log.Printf("warning: %s: %v", name, err)
|
|
continue
|
|
}
|
|
if fi.Mode()&os.ModeSymlink != 0 {
|
|
target, err := os.Readlink(filepath.Join(dir, name))
|
|
if err != nil {
|
|
log.Printf("warning: readlink %s: %v", name, err)
|
|
continue
|
|
}
|
|
packages = append(packages, pkgConf{
|
|
name: name,
|
|
conf: &installerconf.Conf{AliasOf: target},
|
|
})
|
|
continue
|
|
}
|
|
|
|
conf, err := installerconf.Read(path)
|
|
if err != nil {
|
|
log.Printf("warning: %s: %v", path, err)
|
|
continue
|
|
}
|
|
packages = append(packages, pkgConf{name: name, conf: conf})
|
|
}
|
|
|
|
sort.Slice(packages, func(i, j int) bool {
|
|
return packages[i].name < packages[j].name
|
|
})
|
|
return packages, nil
|
|
}
|
|
|
|
// refreshPackage does the full pipeline for one package:
|
|
// fetch raw → classify → write to fsstore.
|
|
func (wc *WebiCache) refreshPackage(ctx context.Context, pkg pkgConf) error {
|
|
pkgStart := time.Now()
|
|
name := pkg.name
|
|
conf := pkg.conf
|
|
|
|
// Step 1: Fetch raw upstream data to rawcache (unless -no-fetch).
|
|
if !wc.NoFetch {
|
|
shallow := wc.Shallow
|
|
if !shallow {
|
|
d, err := rawcache.Open(filepath.Join(wc.RawDir, name))
|
|
if err == nil && d.Populated() {
|
|
shallow = true
|
|
}
|
|
}
|
|
fetchStart := time.Now()
|
|
if err := wc.fetchRaw(ctx, pkg, shallow); err != nil {
|
|
return fmt.Errorf("fetch: %w", err)
|
|
}
|
|
log.Printf(" %s: fetch %s", name, time.Since(fetchStart))
|
|
}
|
|
|
|
// Step 2: Classify raw data into assets, tag variants, apply config.
|
|
classifyStart := time.Now()
|
|
d, err := rawcache.Open(filepath.Join(wc.RawDir, name))
|
|
if err != nil {
|
|
return fmt.Errorf("rawcache open: %w", err)
|
|
}
|
|
|
|
// Open supplementary gittag raw cache if available (for packages with
|
|
// git_url that use a non-gittag source type like servicemandist).
|
|
var gitTagDir *rawcache.Dir
|
|
if conf.GitURL != "" && conf.Source != "gittag" {
|
|
gd, gdErr := rawcache.Open(filepath.Join(wc.RawDir, "_gittag", name))
|
|
if gdErr == nil && gd.Populated() {
|
|
gitTagDir = gd
|
|
}
|
|
}
|
|
|
|
assets, err := classifypkg.Package(name, conf, d, gitTagDir)
|
|
if err != nil {
|
|
return fmt.Errorf("classify: %w", err)
|
|
}
|
|
classifyDur := time.Since(classifyStart)
|
|
|
|
// Step 3: Write to fsstore.
|
|
writeStart := time.Now()
|
|
tx, err := wc.Store.BeginRefresh(ctx, name)
|
|
if err != nil {
|
|
return fmt.Errorf("begin refresh: %w", err)
|
|
}
|
|
if err := tx.Put(assets); err != nil {
|
|
tx.Rollback()
|
|
return fmt.Errorf("put: %w", err)
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return fmt.Errorf("commit: %w", err)
|
|
}
|
|
writeDur := time.Since(writeStart)
|
|
|
|
log.Printf(" %s: %d assets (classify %s, write %s, total %s)",
|
|
name, len(assets), classifyDur, writeDur, time.Since(pkgStart))
|
|
return nil
|
|
}
|
|
|
|
// --- Fetch raw ---
|
|
|
|
func (wc *WebiCache) fetchRaw(ctx context.Context, pkg pkgConf, shallow bool) error {
|
|
switch pkg.conf.Source {
|
|
case "github", "githubsource":
|
|
if err := wc.fetchGitHub(ctx, pkg.name, pkg.conf, shallow); err != nil {
|
|
return err
|
|
}
|
|
case "nodedist":
|
|
return wc.fetchNodeDist(ctx, pkg.name, pkg.conf)
|
|
case "gittag":
|
|
return wc.fetchGitTag(ctx, pkg.name, pkg.conf, shallow)
|
|
case "gitea":
|
|
return wc.fetchGitea(ctx, pkg.name, pkg.conf, shallow)
|
|
case "chromedist":
|
|
return fetchChromeDist(ctx, wc.Client, wc.RawDir, pkg.name)
|
|
case "flutterdist":
|
|
return fetchFlutterDist(ctx, wc.Client, wc.RawDir, pkg.name)
|
|
case "golang":
|
|
return fetchGolang(ctx, wc.Client, wc.RawDir, pkg.name)
|
|
case "gpgdist":
|
|
return fetchGPGDist(ctx, wc.Client, wc.RawDir, pkg.name)
|
|
case "hashicorp":
|
|
return fetchHashiCorp(ctx, wc.Client, wc.RawDir, pkg.name, pkg.conf)
|
|
case "iterm2dist":
|
|
return fetchITerm2Dist(ctx, wc.Client, wc.RawDir, pkg.name)
|
|
case "juliadist":
|
|
return fetchJuliaDist(ctx, wc.Client, wc.RawDir, pkg.name)
|
|
case "mariadbdist":
|
|
return fetchMariaDBDist(ctx, wc.Client, wc.RawDir, pkg.name)
|
|
case "servicemandist":
|
|
if err := servicemandist.Fetch(ctx, wc.Client, wc.RawDir, pkg.name, wc.Auth, shallow); err != nil {
|
|
return err
|
|
}
|
|
case "zigdist":
|
|
return fetchZigDist(ctx, wc.Client, wc.RawDir, pkg.name)
|
|
default:
|
|
log.Printf(" %s: source %q not yet supported, skipping", pkg.name, pkg.conf.Source)
|
|
return nil
|
|
}
|
|
|
|
// For non-gittag sources with a git_url, also clone the repo to get
|
|
// commit hashes. Git entries are classified from this data in
|
|
// refreshPackage, not from the main raw cache.
|
|
if pkg.conf.GitURL != "" && pkg.conf.Source != "gittag" {
|
|
gitShallow := shallow
|
|
if !wc.Shallow {
|
|
gd, gdErr := rawcache.Open(filepath.Join(wc.RawDir, "_gittag", pkg.name))
|
|
if gdErr == nil && !gd.Populated() {
|
|
gitShallow = false
|
|
}
|
|
}
|
|
if err := wc.fetchGitTagSupplementary(ctx, pkg.name, pkg.conf.GitURL, gitShallow); err != nil {
|
|
log.Printf(" %s: supplementary gittag fetch: %v", pkg.name, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// fetchGitTagSupplementary clones a git repo to get commit hashes for
|
|
// packages that use a non-gittag source type (servicemandist, githubsource)
|
|
// but also have a git_url for source installs.
|
|
func (wc *WebiCache) fetchGitTagSupplementary(ctx context.Context, pkgName, gitURL string, shallow bool) error {
|
|
d, err := rawcache.Open(filepath.Join(wc.RawDir, "_gittag", pkgName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
repoDir := filepath.Join(wc.RawDir, "_repos")
|
|
os.MkdirAll(repoDir, 0o755)
|
|
|
|
for batch, err := range gittag.Fetch(ctx, gitURL, repoDir) {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, entry := range batch {
|
|
tag := entry.Version
|
|
if tag == "" {
|
|
tag = "HEAD-" + entry.CommitHash
|
|
}
|
|
data, _ := json.Marshal(entry)
|
|
d.Merge(tag, data)
|
|
}
|
|
if shallow {
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (wc *WebiCache) fetchGitHub(ctx context.Context, pkgName string, conf *installerconf.Conf, shallow bool) error {
|
|
owner, repo := conf.Owner, conf.Repo
|
|
if owner == "" || repo == "" {
|
|
return fmt.Errorf("missing owner or repo")
|
|
}
|
|
|
|
d, err := rawcache.Open(filepath.Join(wc.RawDir, pkgName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tagPrefix := conf.TagPrefix
|
|
for batch, err := range github.Fetch(ctx, wc.Client, owner, repo, wc.Auth) {
|
|
if err != nil {
|
|
return fmt.Errorf("github %s/%s: %w", owner, repo, err)
|
|
}
|
|
for _, rel := range batch {
|
|
if rel.Draft {
|
|
continue
|
|
}
|
|
tag := rel.TagName
|
|
if tagPrefix != "" && !strings.HasPrefix(tag, tagPrefix) {
|
|
continue
|
|
}
|
|
data, _ := json.Marshal(rel)
|
|
d.Merge(tag, data)
|
|
}
|
|
if shallow {
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (wc *WebiCache) fetchNodeDist(ctx context.Context, pkgName string, conf *installerconf.Conf) error {
|
|
baseURL := conf.BaseURL
|
|
if baseURL == "" {
|
|
return fmt.Errorf("missing url")
|
|
}
|
|
|
|
d, err := rawcache.Open(filepath.Join(wc.RawDir, pkgName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Fetch from primary URL. Tag with "official/" prefix so unofficial
|
|
// entries for the same version don't overwrite.
|
|
for batch, err := range nodedist.Fetch(ctx, wc.Client, baseURL) {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, entry := range batch {
|
|
data, _ := json.Marshal(entry)
|
|
d.Merge("official/"+entry.Version, data)
|
|
}
|
|
}
|
|
|
|
// Fetch from unofficial URL if configured (e.g. Node.js unofficial builds
|
|
// which add musl, riscv64, loong64 targets).
|
|
if unofficialURL := conf.Extra["unofficial_url"]; unofficialURL != "" {
|
|
for batch, err := range nodedist.Fetch(ctx, wc.Client, unofficialURL) {
|
|
if err != nil {
|
|
log.Printf("warning: %s unofficial fetch: %v", pkgName, err)
|
|
break
|
|
}
|
|
for _, entry := range batch {
|
|
data, _ := json.Marshal(entry)
|
|
d.Merge("unofficial/"+entry.Version, data)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (wc *WebiCache) fetchGitTag(ctx context.Context, pkgName string, conf *installerconf.Conf, shallow bool) error {
|
|
gitURL := conf.BaseURL
|
|
if gitURL == "" {
|
|
return fmt.Errorf("missing url")
|
|
}
|
|
|
|
d, err := rawcache.Open(filepath.Join(wc.RawDir, pkgName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
repoDir := filepath.Join(wc.RawDir, "_repos")
|
|
os.MkdirAll(repoDir, 0o755)
|
|
|
|
for batch, err := range gittag.Fetch(ctx, gitURL, repoDir) {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, entry := range batch {
|
|
tag := entry.Version
|
|
if tag == "" {
|
|
tag = "HEAD-" + entry.CommitHash
|
|
}
|
|
data, _ := json.Marshal(entry)
|
|
d.Merge(tag, data)
|
|
}
|
|
if shallow {
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (wc *WebiCache) fetchGitea(ctx context.Context, pkgName string, conf *installerconf.Conf, shallow bool) error {
|
|
baseURL, owner, repo := conf.BaseURL, conf.Owner, conf.Repo
|
|
if baseURL == "" || owner == "" || repo == "" {
|
|
return fmt.Errorf("missing base_url, owner, or repo")
|
|
}
|
|
|
|
d, err := rawcache.Open(filepath.Join(wc.RawDir, pkgName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for batch, err := range gitea.Fetch(ctx, wc.Client, baseURL, owner, repo, nil) {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, rel := range batch {
|
|
if rel.Draft {
|
|
continue
|
|
}
|
|
data, _ := json.Marshal(rel)
|
|
d.Merge(rel.TagName, data)
|
|
}
|
|
if shallow {
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fetchChromeDist(ctx context.Context, client *http.Client, rawDir, pkgName string) error {
|
|
d, err := rawcache.Open(filepath.Join(rawDir, pkgName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for batch, err := range chromedist.Fetch(ctx, client) {
|
|
if err != nil {
|
|
return fmt.Errorf("chromedist: %w", err)
|
|
}
|
|
for _, ver := range batch {
|
|
data, _ := json.Marshal(ver)
|
|
d.Merge(ver.Version, data)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fetchFlutterDist(ctx context.Context, client *http.Client, rawDir, pkgName string) error {
|
|
d, err := rawcache.Open(filepath.Join(rawDir, pkgName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for batch, err := range flutterdist.Fetch(ctx, client) {
|
|
if err != nil {
|
|
return fmt.Errorf("flutterdist: %w", err)
|
|
}
|
|
for _, rel := range batch {
|
|
// Key by version+channel+os for uniqueness.
|
|
key := rel.Version + "-" + rel.Channel + "-" + rel.OS
|
|
data, _ := json.Marshal(rel)
|
|
d.Merge(key, data)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fetchGolang(ctx context.Context, client *http.Client, rawDir, pkgName string) error {
|
|
d, err := rawcache.Open(filepath.Join(rawDir, pkgName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for batch, err := range golang.Fetch(ctx, client) {
|
|
if err != nil {
|
|
return fmt.Errorf("golang: %w", err)
|
|
}
|
|
for _, rel := range batch {
|
|
data, _ := json.Marshal(rel)
|
|
d.Merge(rel.Version, data)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fetchGPGDist(ctx context.Context, client *http.Client, rawDir, pkgName string) error {
|
|
d, err := rawcache.Open(filepath.Join(rawDir, pkgName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for batch, err := range gpgdist.Fetch(ctx, client) {
|
|
if err != nil {
|
|
return fmt.Errorf("gpgdist: %w", err)
|
|
}
|
|
for _, entry := range batch {
|
|
data, _ := json.Marshal(entry)
|
|
d.Merge(entry.Version, data)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fetchHashiCorp(ctx context.Context, client *http.Client, rawDir, pkgName string, conf *installerconf.Conf) error {
|
|
product := conf.Repo
|
|
if product == "" {
|
|
product = pkgName
|
|
}
|
|
|
|
d, err := rawcache.Open(filepath.Join(rawDir, pkgName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for idx, err := range hashicorp.Fetch(ctx, client, product) {
|
|
if err != nil {
|
|
return fmt.Errorf("hashicorp %s: %w", product, err)
|
|
}
|
|
for ver, vdata := range idx.Versions {
|
|
data, _ := json.Marshal(vdata)
|
|
d.Merge(ver, data)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fetchITerm2Dist(ctx context.Context, client *http.Client, rawDir, pkgName string) error {
|
|
d, err := rawcache.Open(filepath.Join(rawDir, pkgName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for batch, err := range iterm2dist.Fetch(ctx, client) {
|
|
if err != nil {
|
|
return fmt.Errorf("iterm2dist: %w", err)
|
|
}
|
|
for _, entry := range batch {
|
|
key := entry.Version
|
|
if entry.Channel == "beta" {
|
|
key += "-beta"
|
|
}
|
|
data, _ := json.Marshal(entry)
|
|
d.Merge(key, data)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fetchJuliaDist(ctx context.Context, client *http.Client, rawDir, pkgName string) error {
|
|
d, err := rawcache.Open(filepath.Join(rawDir, pkgName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for batch, err := range juliadist.Fetch(ctx, client) {
|
|
if err != nil {
|
|
return fmt.Errorf("juliadist: %w", err)
|
|
}
|
|
for _, rel := range batch {
|
|
data, _ := json.Marshal(rel)
|
|
d.Merge(rel.Version, data)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fetchMariaDBDist(ctx context.Context, client *http.Client, rawDir, pkgName string) error {
|
|
d, err := rawcache.Open(filepath.Join(rawDir, pkgName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for batch, err := range mariadbdist.Fetch(ctx, client) {
|
|
if err != nil {
|
|
return fmt.Errorf("mariadbdist: %w", err)
|
|
}
|
|
for _, rel := range batch {
|
|
data, _ := json.Marshal(rel)
|
|
d.Merge(rel.ReleaseID, data)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fetchZigDist(ctx context.Context, client *http.Client, rawDir, pkgName string) error {
|
|
d, err := rawcache.Open(filepath.Join(rawDir, pkgName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for batch, err := range zigdist.Fetch(ctx, client) {
|
|
if err != nil {
|
|
return fmt.Errorf("zigdist: %w", err)
|
|
}
|
|
for _, rel := range batch {
|
|
data, _ := json.Marshal(rel)
|
|
d.Merge(rel.Version, data)
|
|
}
|
|
}
|
|
return nil
|
|
}
|