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>
This commit is contained in:
@@ -1,2 +1,116 @@
|
||||
// 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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user