Skip to content
Merged
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
50 changes: 29 additions & 21 deletions core/commands/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/ipfs/kubo/core/commands/cmdenv"
"github.com/ipfs/kubo/core/commands/cmdutils"

"github.com/cheggaaa/pb"
"github.com/cheggaaa/pb/v3"
"github.com/ipfs/boxo/files"
uio "github.com/ipfs/boxo/ipld/unixfs/io"
mfs "github.com/ipfs/boxo/mfs"
Expand Down Expand Up @@ -76,6 +76,11 @@ const (

const (
adderOutChanSize = 8

// pb/v3 template used before the upload total is known: only the
// running byte counter and current speed. Swapped for
// cmdenv.ProgressBarFullTemplate once size discovery reports.
progressBarInitTemplate = `{{counters . }} {{speed . "%s/s" "?/s"}}`
)

var AddCmd = &cmds.Command{
Expand Down Expand Up @@ -252,7 +257,7 @@ https://github.com/ipfs/kubo/blob/master/docs/config.md#import
cmds.BoolOption(quietOptionName, "q", "Write minimal output."),
cmds.BoolOption(quieterOptionName, "Q", "Write only final hash."),
cmds.BoolOption(silentOptionName, "Write no output."),
cmds.BoolOption(progressOptionName, "p", "Stream progress data."),
cmds.BoolOption(progressOptionName, "p", "Stream progress data. Defaults to true when stderr is a terminal."),
// Basic Add Behavior
cmds.BoolOption(onlyHashOptionName, "n", "Only chunk and hash - do not write to disk."),
cmds.BoolOption(wrapOptionName, "w", "Wrap files with a directory object."),
Expand Down Expand Up @@ -292,10 +297,10 @@ https://github.com/ipfs/kubo/blob/master/docs/config.md#import
silent, _ := req.Options[silentOptionName].(bool)

if !quiet && !silent {
// ipfs cli progress bar defaults to true unless quiet or silent is used
// default to showing progress only when stderr is a terminal
_, found := req.Options[progressOptionName].(bool)
if !found {
req.Options[progressOptionName] = true
req.Options[progressOptionName] = cmdenv.IsTerminal(os.Stderr)
}
}

Expand Down Expand Up @@ -732,11 +737,8 @@ https://github.com/ipfs/kubo/blob/master/docs/config.md#import

var bar *pb.ProgressBar
if progress {
bar = pb.New64(0).SetUnits(pb.U_BYTES)
bar.ManualUpdate = true
bar.ShowTimeLeft = false
bar.ShowPercent = false
bar.Output = os.Stderr
bar = pb.New64(0).Set(pb.Bytes, true).Set(pb.Static, true).SetWriter(os.Stderr)
bar.SetTemplateString(progressBarInitTemplate)
bar.Start()
}

Expand Down Expand Up @@ -786,31 +788,37 @@ https://github.com/ipfs/kubo/blob/master/docs/config.md#import
}
lastBytes = output.Bytes
delta := prevFiles + lastBytes - totalProgress
totalProgress = bar.Add64(delta)
bar.Add64(delta)
totalProgress = bar.Current()
}

if progress {
bar.Update()
bar.Write()
}
case size := <-sizeChan:
if progress {
bar.Total = size
bar.ShowPercent = true
bar.ShowBar = true
bar.ShowTimeLeft = true
bar.SetTotal(size)
bar.SetTemplateString(cmdenv.ProgressBarFullTemplate)
}
case <-req.Context.Done():
// don't set or print error here, that happens in the goroutine below
return
}
}

if progress && bar.Total == 0 && bar.Get() != 0 {
bar.Total = bar.Get()
bar.ShowPercent = true
bar.ShowBar = true
bar.ShowTimeLeft = true
bar.Update()
if progress {
// If size discovery never reported, treat the
// observed bytes as the total so the final frame
// renders the bar and percent.
if bar.Total() == 0 && bar.Current() != 0 {
bar.SetTotal(bar.Current())
bar.SetTemplateString(cmdenv.ProgressBarFullTemplate)
}
// Finish first so the speed element switches to
// the absolute-rate branch (total/elapsed) when
// EWMA never accumulated a sample on fast adds.
bar.Finish()
bar.Write()
}
}

Expand Down
8 changes: 3 additions & 5 deletions core/commands/cat.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/ipfs/kubo/core/commands/cmdenv"
"github.com/ipfs/kubo/core/commands/cmdutils"

"github.com/cheggaaa/pb"
"github.com/cheggaaa/pb/v3"
"github.com/ipfs/boxo/files"
cmds "github.com/ipfs/go-ipfs-cmds"
iface "github.com/ipfs/kubo/core/coreiface"
Expand All @@ -34,7 +34,7 @@ var CatCmd = &cmds.Command{
Options: []cmds.Option{
cmds.Int64Option(offsetOptionName, "o", "Byte offset to begin reading from."),
cmds.Int64Option(lengthOptionName, "l", "Maximum number of bytes to read."),
cmds.BoolOption(progressOptionName, "p", "Stream progress data.").WithDefault(true),
cmds.BoolOption(progressOptionName, "p", "Stream progress data. Defaults to true when stderr is a terminal."),
},
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
api, err := cmdenv.GetApi(env, req)
Expand Down Expand Up @@ -101,9 +101,7 @@ var CatCmd = &cmds.Command{
case io.Reader:
reader := val

req := res.Request()
progress, _ := req.Options[progressOptionName].(bool)
if progress {
if cmdenv.ShouldShowProgress(res.Request(), progressOptionName) {
var bar *pb.ProgressBar
bar, reader = progressBarForReader(os.Stderr, val, int64(res.Length()))
bar.Start()
Expand Down
24 changes: 24 additions & 0 deletions core/commands/cmdenv/progress.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cmdenv

import (
"os"

cmds "github.com/ipfs/go-ipfs-cmds"
)

// ProgressBarFullTemplate is the pb/v3 template used by transfer
// commands once the total byte count is known: byte counter, bar,
// speed, percent, and ETA. Explicit format args override pb's
// defaults so the rate renders as "MiB/s" (not "MiB p/s") and the
// remaining time falls back to "ETA ?" while speed is unknown.
const ProgressBarFullTemplate = `{{counters . }} {{bar . }} {{speed . "%s/s" "?/s"}} {{percent . }} {{rtime . "ETA %s" "%s" "ETA ?"}}`

// ShouldShowProgress reports whether a progress bar should be rendered
// based on a boolean option. An explicit `--<flag>=true|false` always
// wins; when unset, it defaults to whether stderr is a terminal.
func ShouldShowProgress(req *cmds.Request, flag string) bool {
if v, ok := req.Options[flag].(bool); ok {
return v
}
return IsTerminal(os.Stderr)
}
46 changes: 46 additions & 0 deletions core/commands/cmdenv/progress_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package cmdenv

import (
"os"
"testing"

cmds "github.com/ipfs/go-ipfs-cmds"
)

func TestShouldShowProgress(t *testing.T) {
const flag = "progress"
makeReq := func(opts map[string]any) *cmds.Request {
if opts == nil {
opts = map[string]any{}
}
return &cmds.Request{Options: opts}
}

t.Run("explicit true wins regardless of TTY", func(t *testing.T) {
if !ShouldShowProgress(makeReq(map[string]any{flag: true}), flag) {
t.Error("expected true for --progress=true")
}
})

t.Run("explicit false wins regardless of TTY", func(t *testing.T) {
if ShouldShowProgress(makeReq(map[string]any{flag: false}), flag) {
t.Error("expected false for --progress=false")
}
})

t.Run("unset defaults to IsTerminal(stderr)", func(t *testing.T) {
got := ShouldShowProgress(makeReq(nil), flag)
want := IsTerminal(os.Stderr)
if got != want {
t.Errorf("ShouldShowProgress(unset) = %v, want IsTerminal(os.Stderr) = %v", got, want)
}
})

t.Run("non-bool value treated as unset", func(t *testing.T) {
got := ShouldShowProgress(makeReq(map[string]any{flag: "yes"}), flag)
want := IsTerminal(os.Stderr)
if got != want {
t.Errorf("ShouldShowProgress(non-bool) = %v, want IsTerminal(os.Stderr) = %v", got, want)
}
})
}
14 changes: 14 additions & 0 deletions core/commands/cmdenv/tty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package cmdenv

import (
"os"

"github.com/mattn/go-isatty"
)

// IsTerminal reports whether f is connected to a terminal,
// including MSYS/Cygwin-style terminals on Windows.
func IsTerminal(f *os.File) bool {
fd := f.Fd()
return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)
}
4 changes: 2 additions & 2 deletions core/commands/dag/dag.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ CAR file follows the CARv1 format: https://ipld.io/specs/transport/car/carv1/
cmds.StringArg("root", true, false, "CID of a root to recursively export").EnableStdin(),
},
Options: []cmds.Option{
cmds.BoolOption(progressOptionName, "p", "Display progress on CLI. Defaults to true when STDERR is a TTY."),
cmds.BoolOption(progressOptionName, "p", "Stream progress data. Defaults to true when stderr is a terminal."),
},
Run: dagExport,
PostRun: cmds.PostRunMap{
Expand Down Expand Up @@ -352,7 +352,7 @@ Note: This command skips duplicate blocks in reporting both size and the number
cmds.StringArg("root", true, true, "CID of a DAG root to get statistics for").EnableStdin(),
},
Options: []cmds.Option{
cmds.BoolOption(progressOptionName, "p", "Show progress on stderr. Auto-detected if stderr is a terminal."),
cmds.BoolOption(progressOptionName, "p", "Stream progress data. Defaults to true when stderr is a terminal."),
},
Run: dagStat,
Type: DagStatSummary{},
Expand Down
31 changes: 11 additions & 20 deletions core/commands/dag/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"os"
"time"

"github.com/cheggaaa/pb"
"github.com/cheggaaa/pb/v3"
cid "github.com/ipfs/go-cid"
cmds "github.com/ipfs/go-ipfs-cmds"
ipld "github.com/ipfs/go-ipld-format"
Expand All @@ -20,6 +20,13 @@ import (
selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse"
)

// pb/v3 template for `ipfs dag export`: byte counter, speed, and
// elapsed time. No bar/percent/ETA because the total size of the
// CAR stream is not known up front. The explicit "%s/s" speed
// format overrides pb's default "p/s" suffix so the rate renders
// as "MiB/s".
const progressBarTemplate = `{{counters . }} {{speed . "%s/s" "?/s"}} {{etime . }}`

func dagExport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
// Accept CID or a content path
p, err := cmdutils.PathOrCidPath(req.Arguments[0])
Expand Down Expand Up @@ -99,28 +106,12 @@ func dagExport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment
}

func finishCLIExport(res cmds.Response, re cmds.ResponseEmitter) error {
var showProgress bool
val, specified := res.Request().Options[progressOptionName]
if !specified {
// default based on TTY availability
errStat, _ := os.Stderr.Stat()
if (errStat.Mode() & os.ModeCharDevice) != 0 {
showProgress = true
}
} else if val.(bool) {
showProgress = true
}

// simple passthrough, no progress
if !showProgress {
if !cmdenv.ShouldShowProgress(res.Request(), progressOptionName) {
return cmds.Copy(re, res)
}

bar := pb.New64(0).SetUnits(pb.U_BYTES)
bar.Output = os.Stderr
bar.ShowSpeed = true
bar.ShowElapsedTime = true
bar.RefreshRate = 500 * time.Millisecond
bar := pb.New64(0).Set(pb.Bytes, true).SetWriter(os.Stderr).SetRefreshRate(500 * time.Millisecond)
bar.SetTemplateString(progressBarTemplate)
bar.Start()

var processedOneResponse bool
Expand Down
12 changes: 1 addition & 11 deletions core/commands/dag/stat.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,7 @@ func dagStat(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment)
}

func finishCLIStat(res cmds.Response, re cmds.ResponseEmitter) error {
// Determine whether to show progress based on TTY detection or explicit flag
var showProgress bool
val, specified := res.Request().Options[progressOptionName]
if !specified {
// Auto-detect: show progress only if stderr is a TTY
if errStat, err := os.Stderr.Stat(); err == nil {
showProgress = (errStat.Mode() & os.ModeCharDevice) != 0
}
} else {
showProgress = val.(bool)
}
showProgress := cmdenv.ShouldShowProgress(res.Request(), progressOptionName)

var dagStats *DagStatSummary
for {
Expand Down
26 changes: 7 additions & 19 deletions core/commands/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"github.com/ipfs/kubo/core/commands/cmdutils"
"github.com/ipfs/kubo/core/commands/e"

"github.com/cheggaaa/pb"
"github.com/cheggaaa/pb/v3"
"github.com/ipfs/boxo/files"
"github.com/ipfs/boxo/tar"
cmds "github.com/ipfs/go-ipfs-cmds"
Expand Down Expand Up @@ -58,7 +58,7 @@ may also specify the level of compression by specifying '-l=<1-9>'.
cmds.BoolOption(archiveOptionName, "a", "Output a TAR archive."),
cmds.BoolOption(compressOptionName, "C", "Compress the output with GZIP compression."),
cmds.IntOption(compressionLevelOptionName, "l", "The level of compression (1-9)."),
cmds.BoolOption(progressOptionName, "p", "Stream progress data.").WithDefault(true),
cmds.BoolOption(progressOptionName, "p", "Stream progress data. Defaults to true when stderr is a terminal."),
},
PreRun: func(req *cmds.Request, env cmds.Environment) error {
_, err := getCompressOptions(req)
Expand Down Expand Up @@ -140,15 +140,15 @@ may also specify the level of compression by specifying '-l=<1-9>'.
}

archive, _ := req.Options[archiveOptionName].(bool)
progress, _ := req.Options[progressOptionName].(bool)
showProgress := cmdenv.ShouldShowProgress(req, progressOptionName)

gw := getWriter{
Out: os.Stdout,
Err: os.Stderr,
Archive: archive,
Compression: cmplvl,
Size: int64(res.Length()),
Progress: progress,
Progress: showProgress,
}

return gw.Write(outReader, outPath)
Expand Down Expand Up @@ -177,19 +177,7 @@ func progressBarForReader(out io.Writer, r io.Reader, l int64) (*pb.ProgressBar,
}

func makeProgressBar(out io.Writer, l int64) *pb.ProgressBar {
// setup bar reader
// TODO: get total length of files
bar := pb.New64(l).SetUnits(pb.U_BYTES)
bar.Output = out

// the progress bar lib doesn't give us a way to get the width of the output,
// so as a hack we just use a callback to measure the output, then get rid of it
bar.Callback = func(line string) {
terminalWidth := len(line)
bar.Callback = nil
log.Infof("terminal width: %v\n", terminalWidth)
}
return bar
return pb.New64(l).Set(pb.Bytes, true).SetTemplateString(cmdenv.ProgressBarFullTemplate).SetWriter(out)
}

func getOutPath(req *cmds.Request) string {
Expand Down Expand Up @@ -260,8 +248,8 @@ func (gw *getWriter) writeExtracted(r io.Reader, fpath string) error {
bar := makeProgressBar(gw.Err, gw.Size)
bar.Start()
defer bar.Finish()
defer bar.Set64(gw.Size)
progressCb = bar.Add64
defer bar.SetCurrent(gw.Size)
progressCb = func(n int64) int64 { bar.Add64(n); return bar.Current() }
}

extractor := &tar.Extractor{Path: fpath, Progress: progressCb}
Expand Down
Loading
Loading