495 lines
9.6 KiB
Go
495 lines
9.6 KiB
Go
package stdout
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"runtime"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"b612.me/apps/b612/gdu/internal/common"
|
|
"b612.me/apps/b612/gdu/pkg/analyze"
|
|
"b612.me/apps/b612/gdu/pkg/device"
|
|
"b612.me/apps/b612/gdu/pkg/fs"
|
|
"b612.me/apps/b612/gdu/report"
|
|
"github.com/fatih/color"
|
|
)
|
|
|
|
// UI struct
|
|
type UI struct {
|
|
*common.UI
|
|
output io.Writer
|
|
red *color.Color
|
|
orange *color.Color
|
|
blue *color.Color
|
|
summarize bool
|
|
noPrefix bool
|
|
top int
|
|
}
|
|
|
|
var (
|
|
progressRunes = []rune(`⠇⠏⠋⠙⠹⠸⠼⠴⠦⠧`)
|
|
progressRunesOld = []rune(`-\\|/`)
|
|
progressRunesCount = len(progressRunes)
|
|
)
|
|
|
|
// CreateStdoutUI creates UI for stdout
|
|
func CreateStdoutUI(
|
|
output io.Writer,
|
|
useColors bool,
|
|
showProgress bool,
|
|
showApparentSize bool,
|
|
showRelativeSize bool,
|
|
summarize bool,
|
|
constGC bool,
|
|
useSIPrefix bool,
|
|
noPrefix bool,
|
|
top int,
|
|
) *UI {
|
|
ui := &UI{
|
|
UI: &common.UI{
|
|
UseColors: useColors,
|
|
ShowProgress: showProgress,
|
|
ShowApparentSize: showApparentSize,
|
|
ShowRelativeSize: showRelativeSize,
|
|
Analyzer: analyze.CreateAnalyzer(),
|
|
ConstGC: constGC,
|
|
UseSIPrefix: useSIPrefix,
|
|
},
|
|
output: output,
|
|
summarize: summarize,
|
|
noPrefix: noPrefix,
|
|
top: top,
|
|
}
|
|
|
|
ui.red = color.New(color.FgRed).Add(color.Bold)
|
|
ui.orange = color.New(color.FgYellow).Add(color.Bold)
|
|
ui.blue = color.New(color.FgBlue).Add(color.Bold)
|
|
|
|
if !useColors {
|
|
color.NoColor = true
|
|
}
|
|
|
|
return ui
|
|
}
|
|
|
|
func (ui *UI) UseOldProgressRunes() {
|
|
progressRunes = progressRunesOld
|
|
progressRunesCount = len(progressRunes)
|
|
}
|
|
|
|
// StartUILoop stub
|
|
func (ui *UI) StartUILoop() error {
|
|
return nil
|
|
}
|
|
|
|
// ListDevices lists mounted devices and shows their disk usage
|
|
func (ui *UI) ListDevices(getter device.DevicesInfoGetter) error {
|
|
devices, err := getter.GetDevicesInfo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
maxDeviceNameLength := maxInt(maxLength(
|
|
devices,
|
|
func(device *device.Device) string { return device.Name },
|
|
), len("Devices"))
|
|
|
|
var sizeLength, percentLength int
|
|
if ui.UseColors {
|
|
sizeLength = 20
|
|
percentLength = 16
|
|
} else {
|
|
sizeLength = 9
|
|
percentLength = 5
|
|
}
|
|
|
|
lineFormat := fmt.Sprintf(
|
|
"%%%ds %%%ds %%%ds %%%ds %%%ds %%s\n",
|
|
maxDeviceNameLength,
|
|
sizeLength,
|
|
sizeLength,
|
|
sizeLength,
|
|
percentLength,
|
|
)
|
|
|
|
fmt.Fprintf(
|
|
ui.output,
|
|
fmt.Sprintf("%%%ds %%9s %%9s %%9s %%5s %%s\n", maxDeviceNameLength),
|
|
"Device",
|
|
"Size",
|
|
"Used",
|
|
"Free",
|
|
"Used%",
|
|
"Mount point",
|
|
)
|
|
|
|
for _, device := range devices {
|
|
usedPercent := math.Round(float64(device.Size-device.Free) / float64(device.Size) * 100)
|
|
|
|
fmt.Fprintf(
|
|
ui.output,
|
|
lineFormat,
|
|
device.Name,
|
|
ui.formatSize(device.Size),
|
|
ui.formatSize(device.Size-device.Free),
|
|
ui.formatSize(device.Free),
|
|
ui.red.Sprintf("%.f%%", usedPercent),
|
|
device.MountPoint)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AnalyzePath analyzes recursively disk usage in given path
|
|
func (ui *UI) AnalyzePath(path string, _ fs.Item) error {
|
|
var (
|
|
dir fs.Item
|
|
wait sync.WaitGroup
|
|
updateStatsDone chan struct{}
|
|
)
|
|
updateStatsDone = make(chan struct{}, 1)
|
|
|
|
if ui.ShowProgress {
|
|
wait.Add(1)
|
|
go func() {
|
|
defer wait.Done()
|
|
ui.updateProgress(updateStatsDone)
|
|
}()
|
|
}
|
|
|
|
wait.Add(1)
|
|
go func() {
|
|
defer wait.Done()
|
|
dir = ui.Analyzer.AnalyzeDir(path, ui.CreateIgnoreFunc(), ui.ConstGC)
|
|
dir.UpdateStats(make(fs.HardLinkedItems, 10))
|
|
updateStatsDone <- struct{}{}
|
|
}()
|
|
|
|
wait.Wait()
|
|
|
|
switch {
|
|
case ui.top > 0:
|
|
ui.printTopFiles(dir)
|
|
case ui.summarize:
|
|
ui.printTotalItem(dir)
|
|
default:
|
|
ui.showDir(dir)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReadFromStorage reads analysis data from persistent key-value storage
|
|
func (ui *UI) ReadFromStorage(storagePath, path string) error {
|
|
storage := analyze.NewStorage(storagePath, path)
|
|
closeFn := storage.Open()
|
|
defer closeFn()
|
|
|
|
dir, err := storage.GetDirForPath(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch {
|
|
case ui.top > 0:
|
|
ui.printTopFiles(dir)
|
|
case ui.summarize:
|
|
ui.printTotalItem(dir)
|
|
default:
|
|
ui.showDir(dir)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ui *UI) showDir(dir fs.Item) {
|
|
sort.Sort(sort.Reverse(dir.GetFiles()))
|
|
|
|
for _, file := range dir.GetFiles() {
|
|
ui.printItem(file)
|
|
}
|
|
}
|
|
|
|
func (ui *UI) printTopFiles(file fs.Item) {
|
|
collected := analyze.CollectTopFiles(file, ui.top)
|
|
for _, file := range collected {
|
|
ui.printItemPath(file)
|
|
}
|
|
}
|
|
|
|
func (ui *UI) printTotalItem(file fs.Item) {
|
|
var lineFormat string
|
|
if ui.UseColors {
|
|
lineFormat = "%20s %s\n"
|
|
} else {
|
|
lineFormat = "%9s %s\n"
|
|
}
|
|
|
|
var size int64
|
|
if ui.ShowApparentSize {
|
|
size = file.GetSize()
|
|
} else {
|
|
size = file.GetUsage()
|
|
}
|
|
|
|
fmt.Fprintf(
|
|
ui.output,
|
|
lineFormat,
|
|
ui.formatSize(size),
|
|
file.GetName(),
|
|
)
|
|
}
|
|
|
|
func (ui *UI) printItem(file fs.Item) {
|
|
var lineFormat string
|
|
if ui.UseColors {
|
|
lineFormat = "%s %20s %s\n"
|
|
} else {
|
|
lineFormat = "%s %9s %s\n"
|
|
}
|
|
|
|
var size int64
|
|
if ui.ShowApparentSize {
|
|
size = file.GetSize()
|
|
} else {
|
|
size = file.GetUsage()
|
|
}
|
|
|
|
if file.IsDir() {
|
|
fmt.Fprintf(ui.output,
|
|
lineFormat,
|
|
string(file.GetFlag()),
|
|
ui.formatSize(size),
|
|
ui.blue.Sprint("/"+file.GetName()))
|
|
} else {
|
|
fmt.Fprintf(ui.output,
|
|
lineFormat,
|
|
string(file.GetFlag()),
|
|
ui.formatSize(size),
|
|
file.GetName())
|
|
}
|
|
}
|
|
|
|
func (ui *UI) printItemPath(file fs.Item) {
|
|
var lineFormat string
|
|
if ui.UseColors {
|
|
lineFormat = "%20s %s\n"
|
|
} else {
|
|
lineFormat = "%9s %s\n"
|
|
}
|
|
|
|
var size int64
|
|
if ui.ShowApparentSize {
|
|
size = file.GetSize()
|
|
} else {
|
|
size = file.GetUsage()
|
|
}
|
|
|
|
fmt.Fprintf(ui.output,
|
|
lineFormat,
|
|
ui.formatSize(size),
|
|
file.GetPath())
|
|
}
|
|
|
|
// ReadAnalysis reads analysis report from JSON file
|
|
func (ui *UI) ReadAnalysis(input io.Reader) error {
|
|
var (
|
|
dir *analyze.Dir
|
|
wait sync.WaitGroup
|
|
err error
|
|
doneChan chan struct{}
|
|
)
|
|
|
|
if ui.ShowProgress {
|
|
wait.Add(1)
|
|
doneChan = make(chan struct{})
|
|
go func() {
|
|
defer wait.Done()
|
|
ui.showReadingProgress(doneChan)
|
|
}()
|
|
}
|
|
|
|
wait.Add(1)
|
|
go func() {
|
|
defer wait.Done()
|
|
dir, err = report.ReadAnalysis(input)
|
|
if err != nil {
|
|
if ui.ShowProgress {
|
|
doneChan <- struct{}{}
|
|
}
|
|
return
|
|
}
|
|
runtime.GC()
|
|
|
|
dir.UpdateStats(make(fs.HardLinkedItems, 10))
|
|
|
|
if ui.ShowProgress {
|
|
doneChan <- struct{}{}
|
|
}
|
|
}()
|
|
|
|
wait.Wait()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if ui.summarize {
|
|
ui.printTotalItem(dir)
|
|
} else {
|
|
ui.showDir(dir)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ui *UI) showReadingProgress(doneChan chan struct{}) {
|
|
emptyRow := "\r"
|
|
for j := 0; j < 40; j++ {
|
|
emptyRow += " "
|
|
}
|
|
|
|
i := 0
|
|
for {
|
|
fmt.Fprint(ui.output, emptyRow)
|
|
|
|
select {
|
|
case <-doneChan:
|
|
fmt.Fprint(ui.output, "\r")
|
|
return
|
|
default:
|
|
}
|
|
|
|
fmt.Fprintf(ui.output, "\r %s ", string(progressRunes[i]))
|
|
fmt.Fprint(ui.output, "Reading analysis from file...")
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
i++
|
|
i %= progressRunesCount
|
|
}
|
|
}
|
|
|
|
func (ui *UI) updateProgress(updateStatsDone <-chan struct{}) {
|
|
emptyRow := "\r"
|
|
for j := 0; j < 100; j++ {
|
|
emptyRow += " "
|
|
}
|
|
|
|
progressChan := ui.Analyzer.GetProgressChan()
|
|
analysisDoneChan := ui.Analyzer.GetDone()
|
|
|
|
var progress common.CurrentProgress
|
|
|
|
i := 0
|
|
for {
|
|
fmt.Fprint(ui.output, emptyRow)
|
|
|
|
select {
|
|
case progress = <-progressChan:
|
|
case <-analysisDoneChan:
|
|
for {
|
|
fmt.Fprint(ui.output, emptyRow)
|
|
fmt.Fprintf(ui.output, "\r %s ", string(progressRunes[i]))
|
|
fmt.Fprint(ui.output, "Calculating disk usage...")
|
|
time.Sleep(100 * time.Millisecond)
|
|
i++
|
|
i %= progressRunesCount
|
|
|
|
select {
|
|
case <-updateStatsDone:
|
|
fmt.Fprint(ui.output, emptyRow)
|
|
fmt.Fprint(ui.output, "\r")
|
|
return
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(ui.output, "\r %s ", string(progressRunes[i]))
|
|
|
|
fmt.Fprint(ui.output, "Scanning... Total items: "+
|
|
ui.red.Sprint(common.FormatNumber(int64(progress.ItemCount)))+
|
|
" size: "+
|
|
ui.formatSize(progress.TotalSize))
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
i++
|
|
i %= progressRunesCount
|
|
}
|
|
}
|
|
|
|
func (ui *UI) formatSize(size int64) string {
|
|
if ui.noPrefix {
|
|
return ui.orange.Sprintf("%d", size)
|
|
}
|
|
if ui.UseSIPrefix {
|
|
return ui.formatWithDecPrefix(size)
|
|
}
|
|
return ui.formatWithBinPrefix(size)
|
|
}
|
|
|
|
func (ui *UI) formatWithBinPrefix(size int64) string {
|
|
fsize := float64(size)
|
|
asize := math.Abs(fsize)
|
|
|
|
switch {
|
|
case asize >= common.Ei:
|
|
return ui.orange.Sprintf("%.1f", fsize/common.Ei) + " EiB"
|
|
case asize >= common.Pi:
|
|
return ui.orange.Sprintf("%.1f", fsize/common.Pi) + " PiB"
|
|
case asize >= common.Ti:
|
|
return ui.orange.Sprintf("%.1f", fsize/common.Ti) + " TiB"
|
|
case asize >= common.Gi:
|
|
return ui.orange.Sprintf("%.1f", fsize/common.Gi) + " GiB"
|
|
case asize >= common.Mi:
|
|
return ui.orange.Sprintf("%.1f", fsize/common.Mi) + " MiB"
|
|
case asize >= common.Ki:
|
|
return ui.orange.Sprintf("%.1f", fsize/common.Ki) + " KiB"
|
|
default:
|
|
return ui.orange.Sprintf("%d", size) + " B"
|
|
}
|
|
}
|
|
|
|
func (ui *UI) formatWithDecPrefix(size int64) string {
|
|
fsize := float64(size)
|
|
asize := math.Abs(fsize)
|
|
|
|
switch {
|
|
case asize >= common.E:
|
|
return ui.orange.Sprintf("%.1f", fsize/common.E) + " EB"
|
|
case asize >= common.P:
|
|
return ui.orange.Sprintf("%.1f", fsize/common.P) + " PB"
|
|
case asize >= common.T:
|
|
return ui.orange.Sprintf("%.1f", fsize/common.T) + " TB"
|
|
case asize >= common.G:
|
|
return ui.orange.Sprintf("%.1f", fsize/common.G) + " GB"
|
|
case asize >= common.M:
|
|
return ui.orange.Sprintf("%.1f", fsize/common.M) + " MB"
|
|
case asize >= common.K:
|
|
return ui.orange.Sprintf("%.1f", fsize/common.K) + " kB"
|
|
default:
|
|
return ui.orange.Sprintf("%d", size) + " B"
|
|
}
|
|
}
|
|
|
|
func maxLength(list []*device.Device, keyGetter func(*device.Device) string) int {
|
|
maxLen := 0
|
|
var s string
|
|
for _, item := range list {
|
|
s = keyGetter(item)
|
|
if len(s) > maxLen {
|
|
maxLen = len(s)
|
|
}
|
|
}
|
|
return maxLen
|
|
}
|
|
|
|
func maxInt(x, y int) int {
|
|
if x > y {
|
|
return x
|
|
}
|
|
return y
|
|
}
|