git-lfs/progress/meter.go
2016-12-07 13:18:33 -07:00

217 lines
5.3 KiB
Go

package progress
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/olekukonko/ts"
)
// ProgressMeter provides a progress bar type output for the TransferQueue. It
// is given an estimated file count and size up front and tracks the number of
// files and bytes transferred as well as the number of files and bytes that
// get skipped because the transfer is unnecessary.
type ProgressMeter struct {
finishedFiles int64 // int64s must come first for struct alignment
skippedFiles int64
transferringFiles int64
estimatedBytes int64
currentBytes int64
skippedBytes int64
started int32
estimatedFiles int32
startTime time.Time
finished chan interface{}
logger *progressLogger
fileIndex map[string]int64 // Maps a file name to its transfer number
fileIndexMutex *sync.Mutex
DryRun bool
}
type env interface {
Get(key string) (val string, ok bool)
}
type meterOption func(*ProgressMeter)
func WithLogFile(name string) meterOption {
printErr := func(err string) {
fmt.Fprintf(os.Stderr, "Error creating progress logger: %s\n", err)
}
return func(m *ProgressMeter) {
if len(name) == 0 {
return
}
if !filepath.IsAbs(name) {
printErr("GIT_LFS_PROGRESS must be an absolute path")
return
}
cbDir := filepath.Dir(name)
if err := os.MkdirAll(cbDir, 0755); err != nil {
printErr(err.Error())
return
}
file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
printErr(err.Error())
return
}
m.logger.writeData = true
m.logger.log = file
}
}
func WithOSEnv(os env) meterOption {
name, _ := os.Get("GIT_LFS_PROGRESS")
return WithLogFile(name)
}
// NewMeter creates a new ProgressMeter.
func NewMeter(options ...meterOption) *ProgressMeter {
m := &ProgressMeter{
logger: &progressLogger{},
startTime: time.Now(),
fileIndex: make(map[string]int64),
fileIndexMutex: &sync.Mutex{},
finished: make(chan interface{}),
}
for _, opt := range options {
opt(m)
}
return m
}
func (p *ProgressMeter) Start() {
if atomic.SwapInt32(&p.started, 1) == 0 {
go p.writer()
}
}
// Skip tells the progress meter that a file of size `size` is being skipped
// because the transfer is unnecessary.
func (p *ProgressMeter) Skip(size int64) {
atomic.AddInt64(&p.skippedFiles, 1)
atomic.AddInt64(&p.skippedBytes, size)
// Reduce bytes and files so progress easier to parse
atomic.AddInt32(&p.estimatedFiles, -1)
atomic.AddInt64(&p.estimatedBytes, -size)
}
func (p *ProgressMeter) AddEstimate(size int64) {
atomic.AddInt32(&p.estimatedFiles, 1)
atomic.AddInt64(&p.estimatedBytes, size)
}
// StartTransfer tells the progress meter that a transferring file is being
// added to the TransferQueue.
func (p *ProgressMeter) StartTransfer(name string) {
idx := atomic.AddInt64(&p.transferringFiles, 1)
p.fileIndexMutex.Lock()
p.fileIndex[name] = idx
p.fileIndexMutex.Unlock()
}
// TransferBytes increments the number of bytes transferred
func (p *ProgressMeter) TransferBytes(direction, name string, read, total int64, current int) {
atomic.AddInt64(&p.currentBytes, int64(current))
p.logBytes(direction, name, read, total)
}
// FinishTransfer increments the finished transfer count
func (p *ProgressMeter) FinishTransfer(name string) {
atomic.AddInt64(&p.finishedFiles, 1)
p.fileIndexMutex.Lock()
delete(p.fileIndex, name)
p.fileIndexMutex.Unlock()
}
// Finish shuts down the ProgressMeter
func (p *ProgressMeter) Finish() {
close(p.finished)
p.update()
p.logger.Close()
if !p.DryRun && p.estimatedBytes > 0 {
fmt.Fprintf(os.Stdout, "\n")
}
}
func (p *ProgressMeter) logBytes(direction, name string, read, total int64) {
p.fileIndexMutex.Lock()
idx := p.fileIndex[name]
p.fileIndexMutex.Unlock()
line := fmt.Sprintf("%s %d/%d %d/%d %s\n", direction, idx, p.estimatedFiles, 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.DryRun || (p.estimatedFiles == 0 && p.skippedFiles == 0) {
return
}
width := 80 // default to 80 chars wide if ts.GetSize() fails
size, err := ts.GetSize()
if err == nil {
width = size.Col()
}
// (%d of %d files, %d skipped) %f B / %f B, %f B skipped
// skipped counts only show when > 0
out := fmt.Sprintf("\rGit LFS: (%d of %d files", p.finishedFiles, p.estimatedFiles)
if p.skippedFiles > 0 {
out += fmt.Sprintf(", %d skipped", p.skippedFiles)
}
out += fmt.Sprintf(") %s / %s", formatBytes(p.currentBytes), formatBytes(p.estimatedBytes))
if p.skippedBytes > 0 {
out += fmt.Sprintf(", %s skipped", formatBytes(p.skippedBytes))
}
padlen := width - len(out)
if 0 < padlen {
out += strings.Repeat(" ", padlen)
}
fmt.Fprintf(os.Stdout, out)
}
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)
}