zutils/cmd/dir.go

255 lines
6.1 KiB
Go
Raw Permalink Normal View History

package cmd
import (
"fmt"
"os"
"path/filepath"
"sort"
"time"
ignore "github.com/sabhiram/go-gitignore"
"github.com/zemenawi/zutils/pkg/colors"
"github.com/zemenawi/zutils/pkg/formatter"
)
type FileItem struct {
Path string
Size int64
Ignored bool
RelPath string
}
type DirStats struct {
TotalSize int64
TotalSizeIgnoring int64
FileCount int
DirCount int
IgnoredFileCount int
IgnoredSize int64
LargestFiles []FileItem
LargestDirs []FileItem
LastModified time.Time
FileExtensions map[string]int
HasGitignore bool
}
func DirInfoCommand(args []string) error {
if len(args) < 1 {
return fmt.Errorf("please specify a directory path")
}
dirPath := args[0]
showIgnored := false
if len(args) > 1 && args[1] == "--all" {
showIgnored = true
}
stats, err := analyzeDirectory(dirPath, showIgnored)
if err != nil {
return fmt.Errorf("error analyzing directory: %w", err)
}
size := formatter.FormatSize(stats.TotalSize)
ignoringSize := formatter.FormatSize(stats.TotalSizeIgnoring)
modTime := stats.LastModified.Format(time.RFC1123)
PrintBoxHeader("DIRECTORY INFORMATION", colors.Purple)
fmt.Printf("%s%s📁 Path:%s %s\n", colors.Blue, colors.Bold, colors.Reset, dirPath)
fmt.Printf("%s%s📏 Total Size:%s %s\n", colors.Blue, colors.Bold, colors.Reset, size)
if stats.HasGitignore {
fmt.Printf("%s%s📏 Size (excl. ignored):%s %s\n", colors.Blue, colors.Bold, colors.Reset, ignoringSize)
fmt.Printf("%s%s🚫 Ignored:%s %d files (%s)\n", colors.Gray, colors.Bold, colors.Reset, stats.IgnoredFileCount, formatter.FormatSize(stats.IgnoredSize))
}
fmt.Printf("%s%s📊 Files:%s %d\n", colors.Blue, colors.Bold, colors.Reset, stats.FileCount)
fmt.Printf("%s%s📂 Directories:%s %d\n", colors.Blue, colors.Bold, colors.Reset, stats.DirCount)
fmt.Println()
if len(stats.LargestFiles) > 0 {
PrintSectionHeader("LARGEST FILES")
headers := []string{"#", "File Name", "Size"}
if stats.HasGitignore {
headers = []string{"#", "File Name", "Size", "Status"}
}
data := make([][]string, 0, len(stats.LargestFiles))
for i, file := range stats.LargestFiles {
name := file.RelPath
row := []string{
fmt.Sprintf("%d", i+1),
name,
formatter.FormatSize(file.Size),
}
if stats.HasGitignore {
if file.Ignored {
row = append(row, colors.Gray+"[ignored]"+colors.Reset)
} else {
row = append(row, "")
}
}
data = append(data, row)
}
printSimpleTable(headers, data)
}
if len(stats.LargestDirs) > 0 {
PrintSectionHeader("LARGEST DIRS")
headers := []string{"#", "Directory Name", "Size"}
data := make([][]string, 0, len(stats.LargestDirs))
for i, dir := range stats.LargestDirs {
name := dir.RelPath
row := []string{
fmt.Sprintf("%d", i+1),
name,
formatter.FormatSize(dir.Size),
}
data = append(data, row)
}
printSimpleTable(headers, data)
}
if len(stats.FileExtensions) > 0 {
PrintSectionHeader("FILE TYPE DISTRIBUTION")
type extCount struct {
ext string
count int
}
var sortedExts []extCount
for ext, count := range stats.FileExtensions {
sortedExts = append(sortedExts, extCount{ext, count})
}
sort.Slice(sortedExts, func(i, j int) bool {
return sortedExts[i].count > sortedExts[j].count
})
maxShow := 10
if len(sortedExts) < maxShow {
maxShow = len(sortedExts)
}
headers := []string{"#", "Extension", "File Count", "Percentage"}
data := make([][]string, 0, maxShow)
for i := 0; i < maxShow; i++ {
ext := sortedExts[i].ext
if ext == "" {
ext = "(no extension)"
}
percentage := float64(sortedExts[i].count) / float64(stats.FileCount) * 100
row := []string{
fmt.Sprintf("%d", i+1),
ext,
fmt.Sprintf("%d", sortedExts[i].count),
fmt.Sprintf("%.1f%%", percentage),
}
data = append(data, row)
}
printSimpleTable(headers, data)
}
PrintSectionHeader("DIRECTORY DETAILS")
fmt.Printf("%s%s📅 Last Modified:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, modTime)
if stats.HasGitignore {
fmt.Printf("%s%s📜 Gitignore:%s %s\n", colors.Green, colors.Bold, colors.Reset, "Detected")
}
fmt.Println()
return nil
}
func printSimpleTable(headers []string, data [][]string) {
formatter.PrintTable(headers, data, colors.Cyan)
fmt.Println()
}
func analyzeDirectory(path string, showIgnored bool) (*DirStats, error) {
stats := &DirStats{
LargestFiles: make([]FileItem, 0),
LargestDirs: make([]FileItem, 0),
FileExtensions: make(map[string]int),
}
ignoreMatcher, err := ignore.CompileIgnoreFile(filepath.Join(path, ".gitignore"))
if err == nil {
stats.HasGitignore = true
_ = ignoreMatcher
}
err = filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, _ := filepath.Rel(path, filePath)
if filePath == path {
return nil
}
ignored := false
if stats.HasGitignore {
ignored = ignoreMatcher.MatchesPath(relPath)
}
if info.IsDir() {
if !ignored || showIgnored {
stats.DirCount++
stats.LargestDirs = appendSorted(stats.LargestDirs, FileItem{
Path: filePath,
Size: info.Size(),
Ignored: ignored,
RelPath: relPath,
}, 5)
}
} else {
stats.FileCount++
stats.TotalSize += info.Size()
if ignored {
stats.IgnoredFileCount++
stats.IgnoredSize += info.Size()
} else {
stats.TotalSizeIgnoring += info.Size()
}
ext := filepath.Ext(filePath)
stats.FileExtensions[ext]++
if !ignored || showIgnored {
stats.LargestFiles = appendSorted(stats.LargestFiles, FileItem{
Path: filePath,
Size: info.Size(),
Ignored: ignored,
RelPath: relPath,
}, 5)
}
if info.ModTime().After(stats.LastModified) {
stats.LastModified = info.ModTime()
}
}
return nil
})
return stats, err
}
func appendSorted(items []FileItem, newItem FileItem, maxSize int) []FileItem {
items = append(items, newItem)
sort.Slice(items, func(i, j int) bool {
return items[i].Size > items[j].Size
})
if len(items) > maxSize {
items = items[:maxSize]
}
return items
}