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 }