Files
Vladimir Zagainov 070b5c0262 feat: add config, auth, fetcher, java modules
- 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>
2026-05-26 06:35:09 +03:00

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()
}