git-lfs/lfs/progress_meter.go
rubyist ab5b35a8c0 Progress meter needs to be more dynamic for batches of n
This removes the `pb` based progress bar for the transfer queue and add
a simpler custom bar. pb really wants to know total counts and byte
sizes up front but when doing batches of n we would prefer to not know
that. This is due to the fact that the batcher will block until it is
drained instead of processing all pointers and storing them in memory
while transfer operations run. This also clarifies when files are
skipped because the server already has them. Progress output here
contains the number of finished transfers/bytes, the number of
transfers/bytes currently in the queue, and the number of files skipped,
if any.
2015-07-27 12:43:41 -04:00

207 lines
4.7 KiB
Go

package lfs
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync/atomic"
"time"
"github.com/github/git-lfs/vendor/_nuts/github.com/olekukonko/ts"
)
type ProgressMeter struct {
transferringFiles int64
finishedFiles int64
totalFiles int64
skippedFiles int64
totalBytes int64
currentBytes int64
startTime time.Time
finished chan interface{}
logger *progressLogger
fileIndex map[string]int64 // Maps a file name to its transfer number
show bool
}
type progressEvent int
const (
transferStart = iota
transferBytes
transferFinish
)
func NewProgressMeter() *ProgressMeter {
logger, err := newProgressLogger()
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating progress logger: %s\n", err)
}
pm := &ProgressMeter{
logger: logger,
startTime: time.Now(),
fileIndex: make(map[string]int64),
finished: make(chan interface{}),
show: true,
}
go pm.writer()
return pm
}
func (p *ProgressMeter) Add(name string, size int64) {
atomic.AddInt64(&p.totalBytes, size)
idx := atomic.AddInt64(&p.transferringFiles, 1)
p.fileIndex[name] = idx
}
func (p *ProgressMeter) Skip() {
atomic.AddInt64(&p.skippedFiles, 1)
}
func (p *ProgressMeter) Log(event progressEvent, direction, name string, read, total int64, current int) {
switch event {
case transferBytes:
atomic.AddInt64(&p.currentBytes, int64(current))
p.logBytes(direction, name, read, total)
case transferFinish:
atomic.AddInt64(&p.finishedFiles, 1)
delete(p.fileIndex, name)
}
}
func (p *ProgressMeter) Finish() {
close(p.finished)
p.update()
p.logger.Close()
if p.show {
fmt.Fprintf(os.Stdout, "\n")
}
}
func (p *ProgressMeter) Suppress() {
p.show = false
}
func (p *ProgressMeter) logBytes(direction, name string, read, total int64) {
idx := p.fileIndex[name]
line := fmt.Sprintf("%s %d/%d %d/%d %s\n", direction, idx, p.totalFiles, read, total, name)
if err := p.logger.Write([]byte(line)); err != nil {
p.logger.Shutdown()
}
}
func (p *ProgressMeter) writer() {
p.update()
for {
select {
case <-p.finished:
return
case <-time.After(time.Millisecond * 200):
p.update()
}
}
}
func (p *ProgressMeter) update() {
if !p.show {
return
}
width := 80 // default to 80 chars wide if ts.GetSize() fails
size, err := ts.GetSize()
if err == nil {
width = size.Col()
}
out := fmt.Sprintf("\r(%d of %d files), %s/%s",
p.transferringFiles,
p.finishedFiles,
formatBytes(p.currentBytes),
formatBytes(p.totalBytes))
if skipped := atomic.LoadInt64(&p.skippedFiles); skipped > 0 {
out += fmt.Sprintf(", Skipped: %d", skipped)
}
padding := strings.Repeat(" ", width-len(out))
fmt.Fprintf(os.Stdout, out+padding)
}
// progressLogger provides a wrapper around an os.File that can either
// write to the file or ignore all writes completely.
type progressLogger struct {
writeData bool
log *os.File
}
// Write will write to the file and perform a Sync() if writing succeeds.
func (l *progressLogger) Write(b []byte) error {
if l.writeData {
if _, err := l.log.Write(b); err != nil {
return err
}
return l.log.Sync()
}
return nil
}
// Close will call Close() on the underlying file
func (l *progressLogger) Close() error {
if l.log != nil {
return l.log.Close()
}
return nil
}
// Shutdown will cause the logger to ignore any further writes. It should
// be used when writing causes an error.
func (l *progressLogger) Shutdown() {
l.writeData = false
}
// newProgressLogger creates a progressLogger based on the presence of
// the GIT_LFS_PROGRESS environment variable. If it is present and a log file
// is able to be created, the logger will write to the file. If it is absent,
// or there is an err creating the file, the logger will ignore all writes.
func newProgressLogger() (*progressLogger, error) {
logPath := Config.Getenv("GIT_LFS_PROGRESS")
if len(logPath) == 0 {
return &progressLogger{}, nil
}
if !filepath.IsAbs(logPath) {
return &progressLogger{}, fmt.Errorf("GIT_LFS_PROGRESS must be an absolute path")
}
cbDir := filepath.Dir(logPath)
if err := os.MkdirAll(cbDir, 0755); err != nil {
return &progressLogger{}, err
}
file, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return &progressLogger{}, err
}
return &progressLogger{true, file}, nil
}
func formatBytes(i int64) string {
switch {
case i > 1099511627776:
return fmt.Sprintf("%#0.2f TB", float64(i)/1099511627776)
case i > 1073741824:
return fmt.Sprintf("%#0.2f GB", float64(i)/1073741824)
case i > 1048576:
return fmt.Sprintf("%#0.2f MB", float64(i)/1048576)
case i > 1024:
return fmt.Sprintf("%#0.2f KB", float64(i)/1024)
}
return fmt.Sprintf("%d B", i)
}