263 lines
7.0 KiB
Go
263 lines
7.0 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)
|
|
|
|
// DryRun is an option for NewMeter() that determines whether updates should be
|
|
// sent to stdout.
|
|
func DryRun(dryRun bool) meterOption {
|
|
return func(m *ProgressMeter) {
|
|
m.dryRun = dryRun
|
|
}
|
|
}
|
|
|
|
// WithLogFile is an option for NewMeter() that sends updates to a text file.
|
|
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
|
|
}
|
|
}
|
|
|
|
// WithOSEnv is an option for NewMeter() that sends updates to the text file
|
|
// path specified in the OS Env.
|
|
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
|
|
}
|
|
|
|
// Start begins sending status updates to the optional log file, and stdout.
|
|
func (p *ProgressMeter) Start() {
|
|
if atomic.CompareAndSwapInt32(&p.started, 0, 1) {
|
|
go p.writer()
|
|
}
|
|
}
|
|
|
|
// Pause stops sending status updates temporarily, until Start() is called again.
|
|
func (p *ProgressMeter) Pause() {
|
|
if atomic.CompareAndSwapInt32(&p.started, 1, 0) {
|
|
p.finished <- true
|
|
}
|
|
}
|
|
|
|
// Add tells the progress meter that a single file of the given size will
|
|
// possibly be transferred. If a file doesn't need to be transferred for some
|
|
// reason, be sure to call Skip(int64) with the same size.
|
|
func (p *ProgressMeter) Add(size int64) {
|
|
atomic.AddInt32(&p.estimatedFiles, 1)
|
|
atomic.AddInt64(&p.estimatedBytes, size)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// (%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))
|
|
}
|
|
|
|
fmt.Fprintf(os.Stdout, pad(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)
|
|
}
|
|
|
|
const defaultWidth = 80
|
|
|
|
// pad pads the given message to occupy the entire maximum width of the terminal
|
|
// LFS is attached to. In doing so, this safeguards subsequent prints of shorter
|
|
// messages from leaving stray characters from the previous message on the
|
|
// screen by writing over them with whitespace padding.
|
|
func pad(msg string) string {
|
|
width := defaultWidth
|
|
size, err := ts.GetSize()
|
|
if err == nil {
|
|
// If `ts.GetSize()` was successful, set the width to the number
|
|
// of columns present in the terminal LFS is attached to.
|
|
// Otherwise, fall-back to `defaultWidth`.
|
|
width = size.Col()
|
|
}
|
|
|
|
// Pad the string with whitespace so that printing at the start of the
|
|
// line removes all traces from the last print.removes all traces from
|
|
// the last print.
|
|
padding := strings.Repeat(" ", maxInt(0, width-len(msg)))
|
|
|
|
return msg + padding
|
|
}
|
|
|
|
// maxInt returns the greater of two `int`s, "a", or "b". This function
|
|
// originally comes from `github.com/git-lfs/git-lfs/tools#MaxInt`, but would
|
|
// introduce an import cycle if depended on directly.
|
|
func maxInt(a, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
|
|
return b
|
|
}
|