github.com/jfrog/jfrog-cli-core/v2@v2.52.0/common/progressbar/filesprogressbar.go (about)

     1  package progressbar
     2  
     3  import (
     4  	"errors"
     5  	"net/url"
     6  	"os"
     7  	"strings"
     8  	"sync"
     9  	"sync/atomic"
    10  	"time"
    11  
    12  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    13  
    14  	"github.com/jfrog/jfrog-cli-core/v2/common/commands"
    15  	corelog "github.com/jfrog/jfrog-cli-core/v2/utils/log"
    16  	"github.com/jfrog/jfrog-cli-core/v2/utils/progressbar"
    17  	ioUtils "github.com/jfrog/jfrog-client-go/utils/io"
    18  	"github.com/jfrog/jfrog-client-go/utils/log"
    19  
    20  	"github.com/vbauerster/mpb/v7"
    21  	"github.com/vbauerster/mpb/v7/decor"
    22  )
    23  
    24  type filesProgressBarManager struct {
    25  	// A list of progress bar objects.
    26  	bars []progressBar
    27  	// A wait group for all progress bars.
    28  	barsWg *sync.WaitGroup
    29  	// A container of all external mpb bar objects to be displayed.
    30  	container *mpb.Progress
    31  	// A synchronization lock object.
    32  	barsRWMutex sync.RWMutex
    33  	// A general work indicator spinner.
    34  	headlineBar *mpb.Bar
    35  	// A general tasks completion indicator.
    36  	generalProgressBar *mpb.Bar
    37  	// A cumulative amount of tasks
    38  	tasksCount int64
    39  	// The log file
    40  	logFile *os.File
    41  }
    42  
    43  type progressBarUnit struct {
    44  	bar         *mpb.Bar
    45  	incrChannel chan int
    46  	description string
    47  }
    48  
    49  type progressBar interface {
    50  	ioUtils.Progress
    51  	getProgressBarUnit() *progressBarUnit
    52  }
    53  
    54  func (p *filesProgressBarManager) InitProgressReaders() {
    55  	p.newHeadlineBar(" Working")
    56  	p.tasksCount = 0
    57  	p.newGeneralProgressBar()
    58  }
    59  
    60  // Initializes a new reader progress indicator for a new file transfer.
    61  // Input: 'total' - file size.
    62  //
    63  //	'label' - the title of the operation.
    64  //	'path' - the path of the file being processed.
    65  //
    66  // Output: progress indicator id
    67  func (p *filesProgressBarManager) NewProgressReader(total int64, label, path string) (bar ioUtils.Progress) {
    68  	// Write Lock when appending a new bar to the slice
    69  	p.barsRWMutex.Lock()
    70  	defer p.barsRWMutex.Unlock()
    71  	p.barsWg.Add(1)
    72  	newBar := p.container.New(total,
    73  		mpb.BarStyle().Lbound("|").Filler("🟩").Tip("🟩").Padding("⬛").Refiller("").Rbound("|"),
    74  		mpb.BarRemoveOnComplete(),
    75  		mpb.AppendDecorators(
    76  			// Extra chars length is the max length of the KibiByteCounter
    77  			decor.Name(buildProgressDescription(label, path, progressbar.GetTerminalWidth(), 17)),
    78  			decor.CountersKibiByte("%3.1f/%3.1f"),
    79  		),
    80  	)
    81  
    82  	// Add bar to bars array
    83  	unit := initNewBarUnit(newBar, path)
    84  	barId := len(p.bars) + 1
    85  	readerProgressBar := ReaderProgressBar{progressBarUnit: unit, Id: barId}
    86  	p.bars = append(p.bars, &readerProgressBar)
    87  	return &readerProgressBar
    88  }
    89  
    90  // Initializes a new progress bar, that replaces the progress bar with the given replacedBarId
    91  func (p *filesProgressBarManager) SetMergingState(replacedBarId int, useSpinner bool) (bar ioUtils.Progress) { // Write Lock when appending a new bar to the slice
    92  	p.barsRWMutex.Lock()
    93  	defer p.barsRWMutex.Unlock()
    94  	replacedBar := p.bars[replacedBarId-1].getProgressBarUnit()
    95  	p.bars[replacedBarId-1].Abort()
    96  	newBar := p.container.New(100,
    97  		getMergingProgress(useSpinner),
    98  		mpb.BarRemoveOnComplete(),
    99  		mpb.AppendDecorators(
   100  			decor.Name(buildProgressDescription("  Merging  ", replacedBar.description, progressbar.GetTerminalWidth(), 0)),
   101  		),
   102  	)
   103  	// Bar replacement is a simple spinner and thus does not implement any read functionality
   104  	unit := &progressBarUnit{bar: newBar, description: replacedBar.description}
   105  	progressBar := SimpleProgressBar{progressBarUnit: unit, Id: replacedBarId}
   106  	p.bars[replacedBarId-1] = &progressBar
   107  	return &progressBar
   108  }
   109  
   110  func getMergingProgress(useSpinner bool) mpb.BarFillerBuilder {
   111  	if useSpinner {
   112  		return mpb.SpinnerStyle(createSpinnerFramesArray()...).PositionLeft()
   113  	}
   114  	return mpb.BarStyle().Lbound("|").Filler("🟩").Tip("🟩").Padding("⬛").Refiller("").Rbound("|")
   115  }
   116  
   117  func buildProgressDescription(label, path string, terminalWidth, extraCharsLen int) string {
   118  	separator := " | "
   119  	// Max line length after decreasing bar width (*2 in case unicode chars with double width are used) and the extra chars
   120  	descMaxLength := terminalWidth - (progressbar.ProgressBarWidth*2 + extraCharsLen)
   121  	return buildDescByLimits(descMaxLength, " "+label+separator, shortenUrl(path), separator)
   122  }
   123  
   124  func shortenUrl(path string) string {
   125  	parsedUrl, err := url.ParseRequestURI(path)
   126  	if err != nil {
   127  		return path
   128  	}
   129  	return strings.TrimPrefix(parsedUrl.Path, "/artifactory")
   130  }
   131  
   132  func buildDescByLimits(descMaxLength int, prefix, path, suffix string) string {
   133  	desc := prefix + path + suffix
   134  
   135  	// Verify that the whole description doesn't exceed the max length
   136  	if len(desc) <= descMaxLength {
   137  		return desc
   138  	}
   139  
   140  	// If it does exceed, check if shortening the path will help (+3 is for "...")
   141  	if len(desc)-len(path)+3 > descMaxLength {
   142  		// Still exceeds, do not display desc
   143  		return ""
   144  	}
   145  
   146  	// Shorten path from the beginning
   147  	path = "..." + path[len(desc)-descMaxLength+3:]
   148  	return prefix + path + suffix
   149  }
   150  
   151  func initNewBarUnit(bar *mpb.Bar, path string) *progressBarUnit {
   152  	ch := make(chan int, 1000)
   153  	unit := &progressBarUnit{bar: bar, incrChannel: ch, description: path}
   154  	go incrBarFromChannel(unit)
   155  	return unit
   156  }
   157  
   158  func incrBarFromChannel(unit *progressBarUnit) {
   159  	// Increase bar while channel is open
   160  	for n := range unit.incrChannel {
   161  		unit.bar.IncrBy(n)
   162  	}
   163  }
   164  
   165  func createSpinnerFramesArray() []string {
   166  	black := "⬛"
   167  	green := "🟩"
   168  	spinnerFramesArray := make([]string, progressbar.ProgressBarWidth)
   169  	for i := 1; i < progressbar.ProgressBarWidth-1; i++ {
   170  		cur := "|" + strings.Repeat(black, i-1) + green + strings.Repeat(black, progressbar.ProgressBarWidth-2-i) + "|"
   171  		spinnerFramesArray[i] = cur
   172  	}
   173  	return spinnerFramesArray
   174  }
   175  
   176  // Aborts a progress bar.
   177  // Should be called even if bar completed successfully.
   178  // The progress component's Abort method has no effect if bar has already completed, so can always be safely called anyway
   179  func (p *filesProgressBarManager) RemoveProgress(id int) {
   180  	p.barsRWMutex.RLock()
   181  	defer p.barsWg.Done()
   182  	defer p.barsRWMutex.RUnlock()
   183  	p.bars[id-1].Abort()
   184  }
   185  
   186  // Increases general progress bar by 1
   187  func (p *filesProgressBarManager) IncrementGeneralProgress() {
   188  	p.generalProgressBar.Increment()
   189  }
   190  
   191  // Quits the progress bar while aborting the initial bars.
   192  func (p *filesProgressBarManager) Quit() (err error) {
   193  	if p.headlineBar != nil {
   194  		p.headlineBar.Abort(true)
   195  		p.barsWg.Done()
   196  		p.headlineBar = nil
   197  	}
   198  	if p.generalProgressBar != nil {
   199  		p.generalProgressBar.Abort(true)
   200  		p.barsWg.Done()
   201  		p.generalProgressBar = nil
   202  	}
   203  	// Wait a refresh rate to make sure all aborts have finished
   204  	time.Sleep(progressbar.ProgressRefreshRate)
   205  	p.container.Wait()
   206  	// Close the created log file (once)
   207  	if p.logFile != nil {
   208  		err = corelog.CloseLogFile(p.logFile)
   209  		p.logFile = nil
   210  		// Set back the default logger
   211  		corelog.SetDefaultLogger()
   212  	}
   213  	return
   214  }
   215  
   216  func (p *filesProgressBarManager) GetProgress(id int) ioUtils.Progress {
   217  	return p.bars[id-1]
   218  }
   219  
   220  // Initializes progress bar if possible (all conditions in 'shouldInitProgressBar' are met).
   221  // Returns nil, nil, err if failed.
   222  func InitFilesProgressBarIfPossible(showLogFilePath bool) (ioUtils.ProgressMgr, error) {
   223  	shouldInit, err := progressbar.ShouldInitProgressBar()
   224  	if !shouldInit || err != nil {
   225  		return nil, err
   226  	}
   227  
   228  	logFile, err := corelog.CreateLogFile()
   229  	if err != nil {
   230  		return nil, err
   231  	}
   232  	if showLogFilePath {
   233  		log.Info("Log path:", logFile.Name())
   234  	}
   235  	log.SetLogger(log.NewLogger(corelog.GetCliLogLevel(), logFile))
   236  
   237  	newProgressBar := &filesProgressBarManager{}
   238  	newProgressBar.barsWg = new(sync.WaitGroup)
   239  
   240  	// Initialize the progressBar container with wg, to create a single joint point
   241  	newProgressBar.container = mpb.New(
   242  		mpb.WithOutput(os.Stderr),
   243  		mpb.WithWaitGroup(newProgressBar.barsWg),
   244  		mpb.WithWidth(progressbar.ProgressBarWidth),
   245  		mpb.WithRefreshRate(progressbar.ProgressRefreshRate))
   246  
   247  	newProgressBar.logFile = logFile
   248  
   249  	return newProgressBar, nil
   250  }
   251  
   252  // Initializes a new progress bar for general progress indication
   253  func (p *filesProgressBarManager) newGeneralProgressBar() {
   254  	p.barsWg.Add(1)
   255  	p.generalProgressBar = p.container.New(p.tasksCount,
   256  		mpb.BarStyle().Lbound("|").Filler("⬜").Tip("⬜").Padding("⬛").Refiller("").Rbound("|"),
   257  		mpb.BarRemoveOnComplete(),
   258  		mpb.AppendDecorators(
   259  			decor.Name(" Tasks: "),
   260  			decor.CountersNoUnit("%d/%d"),
   261  		),
   262  	)
   263  }
   264  
   265  // Initializes a new progress bar for headline, with a spinner
   266  func (p *filesProgressBarManager) newHeadlineBar(headline string) {
   267  	p.barsWg.Add(1)
   268  	p.headlineBar = p.container.New(1,
   269  		mpb.SpinnerStyle("∙∙∙∙∙∙", "●∙∙∙∙∙", "∙●∙∙∙∙", "∙∙●∙∙∙", "∙∙∙●∙∙", "∙∙∙∙●∙", "∙∙∙∙∙●", "∙∙∙∙∙∙").PositionLeft(),
   270  		mpb.BarRemoveOnComplete(),
   271  		mpb.PrependDecorators(
   272  			decor.Name(headline),
   273  		),
   274  	)
   275  }
   276  
   277  func (p *filesProgressBarManager) SetHeadlineMsg(msg string) {
   278  	if p.headlineBar != nil {
   279  		current := p.headlineBar
   280  		p.barsRWMutex.RLock()
   281  		// First abort, then mark progress as done and finally release the lock.
   282  		defer p.barsRWMutex.RUnlock()
   283  		defer p.barsWg.Done()
   284  		defer current.Abort(true)
   285  	}
   286  	// Remove emojis from non-supported terminals
   287  	msg = coreutils.RemoveEmojisIfNonSupportedTerminal(msg)
   288  	p.newHeadlineBar(msg)
   289  }
   290  
   291  func (p *filesProgressBarManager) ClearHeadlineMsg() {
   292  	if p.headlineBar != nil {
   293  		p.barsRWMutex.RLock()
   294  		p.headlineBar.Abort(true)
   295  		p.barsWg.Done()
   296  		p.barsRWMutex.RUnlock()
   297  		// Wait a refresh rate to make sure the abort has finished
   298  		time.Sleep(progressbar.ProgressRefreshRate)
   299  	}
   300  	p.headlineBar = nil
   301  }
   302  
   303  // IncGeneralProgressTotalBy increments the general progress bar total count by given n.
   304  func (p *filesProgressBarManager) IncGeneralProgressTotalBy(n int64) {
   305  	atomic.AddInt64(&p.tasksCount, n)
   306  	if p.generalProgressBar != nil {
   307  		p.generalProgressBar.SetTotal(p.tasksCount, false)
   308  	}
   309  }
   310  
   311  type CommandWithProgress interface {
   312  	commands.Command
   313  	SetProgress(ioUtils.ProgressMgr)
   314  }
   315  
   316  func ExecWithProgress(cmd CommandWithProgress) (err error) {
   317  	// Show log file path on all progress bars except 'setup' command
   318  	showLogFilePath := cmd.CommandName() != "setup"
   319  	// Init progress bar.
   320  	progressBar, err := InitFilesProgressBarIfPossible(showLogFilePath)
   321  	if err != nil {
   322  		return err
   323  	}
   324  	if progressBar != nil {
   325  		cmd.SetProgress(progressBar)
   326  		defer func() {
   327  			err = errors.Join(err, progressBar.Quit())
   328  		}()
   329  	}
   330  	err = commands.Exec(cmd)
   331  	return
   332  }