Commit 465039e1 authored by Mickaël Desfrênes's avatar Mickaël Desfrênes
Browse files

add download progression

parent 96b1d685
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -78,6 +78,8 @@ commandes new, projects et cache-clear qui peuvent être lancées aussi en dehor
		"msg.unknown_title":         "inconnu",
		"msg.untitled":              "sans titre",
		"msg.vocabulary_ignored":    "Ce bundle de vocabulaire sera ignoré: %s",
		"msg.download_progress":     "Téléchargement de %s",
		"msg.download_complete":     "Téléchargé %s (%s)",
		"msg.init_new":              "Initialisation d'une nouvelle instance de MaX dans %s",
		"msg.new_ready":             "La nouvelle instance de MaX est prête dans %s",
		"msg.init_existing":         "Initialisation de l'instance de MaX existant dans %s",
@@ -183,6 +185,8 @@ which can also be run outside a MaX directory.`,
		"msg.unknown_title":         "unknown",
		"msg.untitled":              "untitled",
		"msg.vocabulary_ignored":    "This vocabulary bundle will be ignored: %s",
		"msg.download_progress":     "Downloading %s",
		"msg.download_complete":     "Downloaded %s (%s)",
		"msg.init_new":              "Initializing a new MaX instance in %s",
		"msg.new_ready":             "The new MaX instance is ready in %s",
		"msg.init_existing":         "Initializing existing MaX instance in %s",
+113 −1
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import (
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/PuerkitoBio/goquery"
@@ -1032,7 +1033,17 @@ func cachedDownload(source, destination string) error {
	if err != nil {
		return err
	}
	if _, err := io.Copy(f, resp.Body); err != nil {

	writer := io.Writer(f)
	var progress *downloadProgress
	if isInteractiveTTY(os.Stderr) {
		progress = newDownloadProgress(filepath.Base(source), resp.ContentLength)
		progress.start()
		defer progress.finish()
		writer = io.MultiWriter(f, progress)
	}

	if _, err := io.Copy(writer, resp.Body); err != nil {
		f.Close()
		return err
	}
@@ -1045,6 +1056,107 @@ func cachedDownload(source, destination string) error {
	return copyFile(cachePath, destination)
}

type downloadProgress struct {
	label      string
	totalBytes int64
	written    atomic.Int64
	done       chan struct{}
	wg         sync.WaitGroup
	once       sync.Once
}

func newDownloadProgress(label string, totalBytes int64) *downloadProgress {
	if strings.TrimSpace(label) == "" {
		label = "file"
	}
	return &downloadProgress{
		label:      label,
		totalBytes: totalBytes,
		done:       make(chan struct{}),
	}
}

func (p *downloadProgress) Write(b []byte) (int, error) {
	p.written.Add(int64(len(b)))
	return len(b), nil
}

func (p *downloadProgress) start() {
	p.render("|")
	p.wg.Add(1)
	go func() {
		defer p.wg.Done()
		frames := []string{"|", "/", "-", "\\"}
		ticker := time.NewTicker(120 * time.Millisecond)
		defer ticker.Stop()
		frameIndex := 1
		for {
			select {
			case <-p.done:
				return
			case <-ticker.C:
				p.render(frames[frameIndex%len(frames)])
				frameIndex++
			}
		}
	}()
}

func (p *downloadProgress) finish() {
	p.once.Do(func() {
		close(p.done)
		p.wg.Wait()
		fmt.Fprintf(os.Stderr, "\r%s\n", T("msg.download_complete", p.label, formatBytes(p.written.Load())))
	})
}

func (p *downloadProgress) render(frame string) {
	current := p.written.Load()
	base := T("msg.download_progress", p.label)
	if p.totalBytes > 0 {
		percent := int((float64(current) / float64(p.totalBytes)) * 100)
		if percent > 100 {
			percent = 100
		}
		fmt.Fprintf(
			os.Stderr,
			"\r%s %s %3d%% (%s/%s)",
			base,
			frame,
			percent,
			formatBytes(current),
			formatBytes(p.totalBytes),
		)
		return
	}
	fmt.Fprintf(os.Stderr, "\r%s %s %s", base, frame, formatBytes(current))
}

func formatBytes(v int64) string {
	if v < 1024 {
		return fmt.Sprintf("%d B", v)
	}
	units := []string{"KB", "MB", "GB", "TB"}
	size := float64(v)
	unit := -1
	for size >= 1024 && unit < len(units)-1 {
		size /= 1024
		unit++
	}
	return fmt.Sprintf("%.1f %s", size, units[unit])
}

func isInteractiveTTY(f *os.File) bool {
	if f == nil {
		return false
	}
	info, err := f.Stat()
	if err != nil {
		return false
	}
	return (info.Mode() & os.ModeCharDevice) != 0
}

func ensureJava() (string, error) {
	if javaBin, err := exec.LookPath("java"); err == nil {
		return javaBin, nil