// package fetcher handles HTTP downloads and SHA-1 verification. package fetcher import ( "fmt" "io" "net/http" "os" "path/filepath" "sync" "gitea.mrixs.me/Mrixs/MrixsCraft-launcher/pkg/utils" ) // Download downloads a URL to dest, optionally verifying its SHA-1 hash. // onProgress is called with (downloaded, total) bytes; total may be -1 if unknown. func Download(url, dest, expectedSHA1 string, onProgress func(downloaded, total int64)) error { resp, err := http.Get(url) if err != nil { return fmt.Errorf("GET %s: %w", url, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("GET %s → %d", url, resp.StatusCode) } if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { return fmt.Errorf("creating parent for %s: %w", dest, err) } tmp := dest + ".part" f, err := os.Create(tmp) if err != nil { return fmt.Errorf("creating %s: %w", tmp, err) } total := resp.ContentLength var written int64 buf := make([]byte, 32*1024) // 32 KiB buffer for { n, readErr := resp.Body.Read(buf) if n > 0 { if _, writeErr := f.Write(buf[:n]); writeErr != nil { f.Close() os.Remove(tmp) return fmt.Errorf("writing %s: %w", tmp, writeErr) } written += int64(n) if onProgress != nil { onProgress(written, total) } } if readErr == io.EOF { break } if readErr != nil { f.Close() os.Remove(tmp) return fmt.Errorf("reading %s: %w", url, readErr) } } f.Close() // Verify SHA-1 if expected hash provided. if expectedSHA1 != "" { got, err := utils.SHA1File(tmp) if err != nil { os.Remove(tmp) return fmt.Errorf("hashing %s: %w", tmp, err) } if got != expectedSHA1 { os.Remove(tmp) return fmt.Errorf("SHA-1 mismatch: expected %s, got %s", expectedSHA1, got) } } return os.Rename(tmp, dest) } // WorkerPool runs download jobs concurrently with a bounded number of workers. type WorkerPool struct { workers int jobs chan func() wg sync.WaitGroup } // NewWorkerPool creates a pool with the given number of workers. func NewWorkerPool(workers int) *WorkerPool { p := &WorkerPool{ workers: workers, jobs: make(chan func(), 100), } for i := 0; i < workers; i++ { p.wg.Add(1) go func() { defer p.wg.Done() for job := range p.jobs { job() } }() } return p } // Submit adds a job to the pool. func (p *WorkerPool) Submit(job func()) { p.jobs <- job } // Wait blocks until all submitted jobs are finished. func (p *WorkerPool) Wait() { close(p.jobs) p.wg.Wait() }