star/gdu/report/export.go
2025-04-26 19:33:14 +08:00

265 lines
5.7 KiB
Go

package report
import (
"bytes"
"errors"
"fmt"
"io"
"math"
"os"
"sort"
"strconv"
"sync"
"time"
"b612.me/apps/b612/gdu/build"
"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"
"github.com/fatih/color"
)
// UI struct
type UI struct {
*common.UI
output io.Writer
exportOutput io.Writer
red *color.Color
orange *color.Color
writtenChan chan struct{}
}
// CreateExportUI creates UI for stdout
func CreateExportUI(
output io.Writer,
exportOutput io.Writer,
useColors bool,
showProgress bool,
constGC bool,
useSIPrefix bool,
) *UI {
ui := &UI{
UI: &common.UI{
ShowProgress: showProgress,
Analyzer: analyze.CreateAnalyzer(),
ConstGC: constGC,
UseSIPrefix: useSIPrefix,
},
output: output,
exportOutput: exportOutput,
writtenChan: make(chan struct{}),
}
ui.red = color.New(color.FgRed).Add(color.Bold)
ui.orange = color.New(color.FgYellow).Add(color.Bold)
if !useColors {
color.NoColor = true
}
return ui
}
// 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 {
return errors.New("Exporting devices list is not supported")
}
// ReadAnalysis reads analysis report from JSON file
func (ui *UI) ReadAnalysis(input io.Reader) error {
return errors.New("Reading analysis is not possible while exporting")
}
// 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
}
var waitWritten sync.WaitGroup
if ui.ShowProgress {
waitWritten.Add(1)
go func() {
defer waitWritten.Done()
ui.updateProgress()
}()
}
return ui.exportDir(dir, &waitWritten)
}
// AnalyzePath analyzes recursively disk usage in given path
func (ui *UI) AnalyzePath(path string, _ fs.Item) error {
var (
dir fs.Item
wait sync.WaitGroup
waitWritten sync.WaitGroup
)
if ui.ShowProgress {
waitWritten.Add(1)
go func() {
defer waitWritten.Done()
ui.updateProgress()
}()
}
wait.Add(1)
go func() {
defer wait.Done()
dir = ui.Analyzer.AnalyzeDir(path, ui.CreateIgnoreFunc(), ui.ConstGC)
dir.UpdateStats(make(fs.HardLinkedItems, 10))
}()
wait.Wait()
return ui.exportDir(dir, &waitWritten)
}
func (ui *UI) exportDir(dir fs.Item, waitWritten *sync.WaitGroup) error {
sort.Sort(sort.Reverse(dir.GetFiles()))
var (
buff bytes.Buffer
err error
)
buff.Write([]byte(`[1,2,{"progname":"gdu","progver":"`))
buff.Write([]byte(build.Version))
buff.Write([]byte(`","timestamp":`))
buff.Write([]byte(strconv.FormatInt(time.Now().Unix(), 10)))
buff.Write([]byte("},\n"))
if err := dir.EncodeJSON(&buff, true); err != nil {
return err
}
if _, err = buff.Write([]byte("]\n")); err != nil {
return err
}
if _, err = buff.WriteTo(ui.exportOutput); err != nil {
return err
}
if f, ok := ui.exportOutput.(*os.File); ok {
err = f.Close()
if err != nil {
return err
}
}
if ui.ShowProgress {
ui.writtenChan <- struct{}{}
waitWritten.Wait()
}
return nil
}
func (ui *UI) updateProgress() {
waitingForWrite := false
emptyRow := "\r"
for j := 0; j < 100; j++ {
emptyRow += " "
}
progressRunes := []rune(`⠇⠏⠋⠙⠹⠸⠼⠴⠦⠧`)
progressChan := ui.Analyzer.GetProgressChan()
doneChan := ui.Analyzer.GetDone()
var progress common.CurrentProgress
i := 0
for {
fmt.Fprint(ui.output, emptyRow)
select {
case progress = <-progressChan:
case <-doneChan:
fmt.Fprint(ui.output, "\r")
waitingForWrite = true
case <-ui.writtenChan:
fmt.Fprint(ui.output, "\r")
return
default:
}
fmt.Fprintf(ui.output, "\r %s ", string(progressRunes[i]))
if waitingForWrite {
fmt.Fprint(ui.output, "Writing output file...")
} else {
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 %= 10
}
}
func (ui *UI) formatSize(size int64) string {
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"
}
}