- config: OS-specific paths, launcher.json load/save, DefaultSettings with MRIXSCRAFT_SERVER_URL env override - auth: Yggdrasil client (authenticate, refresh, validate), session persistence in session.json, EnsureValid flow - fetcher: HTTP download with SHA-1 verification, WorkerPool for concurrent downloads - java: JRE detection (IsInstalled/Find), platform-specific executable name - utils: SHA1File, SHA1Bytes, Unzip with zip-slip protection - cmd/launcher: wire config + auth into main, session restore on startup Co-Authored-By: OWL <noreply@anthropic.com>
117 lines
2.5 KiB
Go
117 lines
2.5 KiB
Go
// 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()
|
|
}
|