Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 143 additions & 6 deletions setup/internal/installer/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
// use this so you can replace the assets
const limboAssetsEnv = "SKYBLOCK_WORLD_ASSETS"

func DownloadLimboAssets(ctx context.Context, installDir string) error {
func DownloadLimboAssets(ctx context.Context, installDir string, events chan<- Event) error {
configDir := filepath.Join(installDir, "configuration")
if LimboAssetsPresent(configDir) {
return nil
Expand All @@ -25,13 +25,14 @@ func DownloadLimboAssets(ctx context.Context, installDir string) error {
return importLocalLimboAssets(localPath, configDir)
}

reqCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
reqCtx, cancel := context.WithTimeout(ctx, 3*time.Minute)
defer cancel()

req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, LimboAssetsURL, nil)
if err != nil {
return err
}
req.Header.Set("User-Agent", "skyblock-installer/"+Version)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
Expand All @@ -41,6 +42,12 @@ func DownloadLimboAssets(ctx context.Context, installDir string) error {
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return fmt.Errorf("download returned %s", resp.Status)
}
emitAssetProgress(events, "Downloading world assets from %s", LimboAssetsURL)
if resp.ContentLength > 0 {
emitAssetProgress(events, "Remote asset size: %s", formatBytes(resp.ContentLength))
} else {
emitAssetProgress(events, "Remote asset size is unknown; showing downloaded bytes")
}

tmp, err := os.CreateTemp("", "skyblock-limbo-assets-*.zip")
if err != nil {
Expand All @@ -49,15 +56,22 @@ func DownloadLimboAssets(ctx context.Context, installDir string) error {
tmpPath := tmp.Name()
defer os.Remove(tmpPath)

if _, err := io.Copy(tmp, resp.Body); err != nil {
if err := copyWithProgress(reqCtx, cancel, tmp, resp.Body, resp.ContentLength, events); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}

return extractZip(tmpPath, configDir)
emitAssetProgress(events, "Extracting world assets")
if err := extractZip(tmpPath, configDir); err != nil {
return err
}
if !LimboAssetsPresent(configDir) {
return fmt.Errorf("downloaded zip did not contain limbo.schem and world/")
}
return nil
}

func LimboAssetsPresent(configDir string) bool {
Expand All @@ -80,9 +94,132 @@ func importLocalLimboAssets(path, configDir string) error {
return err
}
if info.IsDir() {
return CopyDir(path, configDir)
if err := CopyDir(path, configDir); err != nil {
return err
}
} else if err := extractZip(path, configDir); err != nil {
return err
}
if !LimboAssetsPresent(configDir) {
return fmt.Errorf("local assets did not contain limbo.schem and world/")
}
return nil
}

func copyWithProgress(ctx context.Context, cancel context.CancelFunc, dst io.Writer, src io.Reader, total int64, events chan<- Event) error {
buf := make([]byte, 256*1024)
var downloaded int64
lastEmit := time.Now().Add(-time.Minute)
progress := make(chan struct{}, 1)
done := make(chan struct{})
defer close(done)

go func() {
timer := time.NewTimer(30 * time.Second)
defer timer.Stop()
for {
select {
case <-timer.C:
cancel()
return
case <-progress:
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
timer.Reset(30 * time.Second)
case <-done:
return
}
}
}()

for {
select {
case <-ctx.Done():
return fmt.Errorf("download stalled or timed out after %s: %w", formatBytes(downloaded), ctx.Err())
default:
}

n, readErr := src.Read(buf)
if n > 0 {
written, writeErr := dst.Write(buf[:n])
if writeErr != nil {
return writeErr
}
if written != n {
return io.ErrShortWrite
}
downloaded += int64(n)
select {
case progress <- struct{}{}:
default:
}
if shouldEmitProgress(downloaded, total, lastEmit) {
emitDownloadProgress(events, downloaded, total)
lastEmit = time.Now()
}
}
if readErr == io.EOF {
if downloaded == 0 {
return fmt.Errorf("download returned no data")
}
emitDownloadProgress(events, downloaded, total)
return nil
}
if readErr != nil {
return readErr
}
}
}

func shouldEmitProgress(downloaded, total int64, lastEmit time.Time) bool {
if time.Since(lastEmit) >= 2*time.Second {
return true
}
if total > 0 {
percent := downloaded * 100 / total
lastPercent := (downloaded - 256*1024) * 100 / total
return percent != lastPercent
}
return downloaded%(1024*1024) < 256*1024
}

func emitDownloadProgress(events chan<- Event, downloaded, total int64) {
if total > 0 {
percent := int(downloaded * 100 / total)
emitAssetProgress(events, "World assets %s %s/%s %d%%", progressBar(percent), formatBytes(downloaded), formatBytes(total), percent)
return
}
emitAssetProgress(events, "World assets downloaded %s", formatBytes(downloaded))
}

func progressBar(percent int) string {
const width = 20
if percent < 0 {
percent = 0
}
if percent > 100 {
percent = 100
}
filled := percent * width / 100
return "[" + strings.Repeat("#", filled) + strings.Repeat("-", width-filled) + "]"
}

func formatBytes(bytes int64) string {
const mb = 1024 * 1024
if bytes >= mb {
return fmt.Sprintf("%.1f MB", float64(bytes)/mb)
}
return fmt.Sprintf("%.0f KB", float64(bytes)/1024)
}

func emitAssetProgress(events chan<- Event, format string, args ...any) {
if events != nil {
events <- Event{Line: fmt.Sprintf(format, args...)}
}
return extractZip(path, configDir)
}

func extractZip(zipPath, dst string) error {
Expand Down
2 changes: 1 addition & 1 deletion setup/internal/installer/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (r *Runner) install(ctx context.Context, cfg Config, sourceConfigDir string
}

r.emit("Checking for world assets")
if err := DownloadLimboAssets(ctx, cfg.InstallDir); err != nil {
if err := DownloadLimboAssets(ctx, cfg.InstallDir, r.Events); err != nil {
r.emit("asset import failed: %v", err)
r.emit("Manual fallback: %s", LimboAssetsManualHint())
} else {
Expand Down