Skip to content
Open
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
10 changes: 9 additions & 1 deletion command/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,11 @@ func NewCopyCommandFlags() []cli.Flag {
Aliases: []string{"sp"},
Usage: "show a progress bar",
},
&cli.BoolFlag{
Name: "log-progress",
Aliases: []string{"lp"},
Usage: "show progress in log-friendly format (works in pipes and non-TTY)",
},
}
sharedFlags := NewSharedFlags()
return append(copyFlags, sharedFlags...)
Expand Down Expand Up @@ -358,7 +363,10 @@ func NewCopy(c *cli.Context, deleteSource bool) (*Copy, error) {

var commandProgressBar progressbar.ProgressBar

if c.Bool("show-progress") && !(src.Type == dst.Type) {
// Determine which progress bar to use
if c.Bool("log-progress") && !(src.Type == dst.Type) {
commandProgressBar = progressbar.NewLogProgressBar()
} else if c.Bool("show-progress") && !(src.Type == dst.Type) {
commandProgressBar = progressbar.New()
} else {
commandProgressBar = &progressbar.NoOp{}
Expand Down
129 changes: 129 additions & 0 deletions progressbar/progressbar.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package progressbar

import (
"fmt"
"os"
"sync/atomic"
"time"

"github.com/cheggaaa/pb/v3"
)
Expand Down Expand Up @@ -76,3 +78,130 @@ func (cp *CommandProgressBar) AddCompletedBytes(bytes int64) {
func (cp *CommandProgressBar) AddTotalBytes(bytes int64) {
cp.progressbar.AddTotal(bytes)
}

// LogProgressBar outputs progress to stderr in a log-friendly format
// (newline-separated, no ANSI codes, works in pipes and non-TTY environments)
type LogProgressBar struct {
totalObjects int64
completedObjects int64
totalBytes int64
completedBytes int64
startTime time.Time
lastUpdate time.Time
updateInterval time.Duration
}

var _ ProgressBar = (*LogProgressBar)(nil)

func NewLogProgressBar() *LogProgressBar {
return &LogProgressBar{
updateInterval: 2 * time.Second, // Update every 2 seconds
}
}

func (lp *LogProgressBar) Start() {
lp.startTime = time.Now()
lp.lastUpdate = lp.startTime
}

func (lp *LogProgressBar) Finish() {
lp.print() // Print final status
fmt.Fprintln(os.Stderr, "Transfer complete")
}

func (lp *LogProgressBar) IncrementCompletedObjects() {
atomic.AddInt64(&lp.completedObjects, 1)
lp.maybeUpdate()
}

func (lp *LogProgressBar) IncrementTotalObjects() {
atomic.AddInt64(&lp.totalObjects, 1)
}

func (lp *LogProgressBar) AddCompletedBytes(bytes int64) {
atomic.AddInt64(&lp.completedBytes, bytes)
lp.maybeUpdate()
}

func (lp *LogProgressBar) AddTotalBytes(bytes int64) {
atomic.AddInt64(&lp.totalBytes, bytes)
}

func (lp *LogProgressBar) maybeUpdate() {
now := time.Now()
if now.Sub(lp.lastUpdate) >= lp.updateInterval {
lp.lastUpdate = now
lp.print()
}
}

func (lp *LogProgressBar) print() {
completed := atomic.LoadInt64(&lp.completedBytes)
total := atomic.LoadInt64(&lp.totalBytes)
completedObjs := atomic.LoadInt64(&lp.completedObjects)
totalObjs := atomic.LoadInt64(&lp.totalObjects)

if total == 0 {
return // Nothing to report yet
}

percent := float64(completed) / float64(total) * 100

// Calculate speed and ETA
elapsed := time.Since(lp.startTime).Seconds()
var speed float64
var eta string

if elapsed > 0 {
speed = float64(completed) / elapsed
if speed > 0 {
remaining := float64(total-completed) / speed
eta = formatDuration(time.Duration(remaining) * time.Second)
} else {
eta = "unknown"
}
} else {
eta = "calculating..."
}

// Format output: Progress: X% - Y/Z @ speed - ETA (N/M files)
fmt.Fprintf(os.Stderr, "%.1f%% - %s/%s @ %s/s - ETA: %s (%d/%d files)\n",
percent,
formatBytes(completed),
formatBytes(total),
formatBytes(int64(speed)),
eta,
completedObjs,
totalObjs,
)
}

func formatBytes(bytes int64) string {
const unit = 1000
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "kMGTPE"[exp])
}

func formatDuration(d time.Duration) string {
d = d.Round(time.Second)
h := d / time.Hour
d -= h * time.Hour
m := d / time.Minute
d -= m * time.Minute
s := d / time.Second

if h > 0 {
return fmt.Sprintf("%dh%dm%ds", h, m, s)
}
if m > 0 {
return fmt.Sprintf("%dm%ds", m, s)
}
return fmt.Sprintf("%ds", s)
}
Loading