383 lines
10 KiB
Go
383 lines
10 KiB
Go
|
|
package cmd
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"os"
|
|||
|
|
"path/filepath"
|
|||
|
|
"regexp"
|
|||
|
|
"sort"
|
|||
|
|
"strings"
|
|||
|
|
|
|||
|
|
"github.com/zemenawi/zutils/pkg/colors"
|
|||
|
|
"github.com/zemenawi/zutils/pkg/formatter"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type JunkItem struct {
|
|||
|
|
Path string
|
|||
|
|
Size int64
|
|||
|
|
Type string
|
|||
|
|
SafeToClean bool
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type JunkStats struct {
|
|||
|
|
Items []JunkItem
|
|||
|
|
TotalSize int64
|
|||
|
|
Cleanable int64
|
|||
|
|
NonCleanable int64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var junkPatterns = []struct {
|
|||
|
|
Pattern string
|
|||
|
|
Type string
|
|||
|
|
SafeToClean bool
|
|||
|
|
}{
|
|||
|
|
{".cache", "Cache Directory", true},
|
|||
|
|
{"__pycache__", "Python Bytecode", true},
|
|||
|
|
{".pytest_cache", "Python Test Cache", true},
|
|||
|
|
{".mypy_cache", "Mypy Cache", true},
|
|||
|
|
{"node_modules/.cache", "Node Module Cache", true},
|
|||
|
|
{".npm", "NPM Cache", true},
|
|||
|
|
{".yarn", "Yarn Cache", true},
|
|||
|
|
{".pnpm-store", "PNPM Store", true},
|
|||
|
|
{".next", "Next.js Cache", true},
|
|||
|
|
{".nuxt", "Nuxt Cache", true},
|
|||
|
|
{".output", "Nuxt Output", true},
|
|||
|
|
{".solid", "SolidJS Cache", true},
|
|||
|
|
{".svelte-kit", "SvelteKit Cache", true},
|
|||
|
|
{"dist", "Build Output", true},
|
|||
|
|
{"build", "Build Output", true},
|
|||
|
|
{".gradle/caches", "Gradle Cache", true},
|
|||
|
|
{".cargo/registry/cache", "Cargo Registry Cache", true},
|
|||
|
|
{".composer/cache", "Composer Cache", true},
|
|||
|
|
{".gem/cache", "Ruby Gem Cache", true},
|
|||
|
|
{".sass-cache", "Sass Cache", true},
|
|||
|
|
{".eslintcache", "ESLint Cache", true},
|
|||
|
|
{".webpack", "Webpack Cache", true},
|
|||
|
|
{".parcel-cache", "Parcel Cache", true},
|
|||
|
|
{"thumbs.db", "Windows Thumbnail Cache", true},
|
|||
|
|
{".ds_store", "macOS metadata", true},
|
|||
|
|
{".spotlight", "macOS Spotlight", true},
|
|||
|
|
{".trash", "Trash", true},
|
|||
|
|
{"trash-1000", "Trash", true},
|
|||
|
|
{".cache/thumbnails", "Thumbnail Cache", true},
|
|||
|
|
{".-runtime-version", "Runtime Version", false},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var userHome string
|
|||
|
|
|
|||
|
|
func init() {
|
|||
|
|
userHome, _ = os.UserHomeDir()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func JunksCommand(args []string) error {
|
|||
|
|
if len(args) < 1 {
|
|||
|
|
return listJunks()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
switch args[0] {
|
|||
|
|
case "clean":
|
|||
|
|
return cleanJunks(args[1:])
|
|||
|
|
default:
|
|||
|
|
return fmt.Errorf("unknown subcommand: %s\n\nUsage:\n z junks List junk files and space\n z junks clean Clean junk files", args[0])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func listJunks() error {
|
|||
|
|
fmt.Printf("%s%s🔍 Scanning for junk files...%s\n\n", colors.Yellow, colors.Bold, colors.Reset)
|
|||
|
|
|
|||
|
|
stats, err := scanForJunks()
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("error scanning: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(stats.Items) == 0 {
|
|||
|
|
fmt.Printf("%s✨ No junk files found! Your system is clean.%s\n", colors.Green, colors.Reset)
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
PrintBoxHeader("SYSTEM JUNK ANALYSIS", colors.Red)
|
|||
|
|
|
|||
|
|
fmt.Printf("%s%s💾 Total Junk Size:%s %s\n", colors.Red, colors.Bold, colors.Reset, formatter.FormatSize(stats.TotalSize))
|
|||
|
|
fmt.Printf("%s%s✅ Cleanable:%s %s\n", colors.Green, colors.Bold, colors.Reset, formatter.FormatSize(stats.Cleanable))
|
|||
|
|
fmt.Printf("%s%s⚠️ Non-cleanable:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, formatter.FormatSize(stats.NonCleanable))
|
|||
|
|
fmt.Println()
|
|||
|
|
|
|||
|
|
byType := groupByType(stats.Items)
|
|||
|
|
|
|||
|
|
type typeGroup struct {
|
|||
|
|
junkType string
|
|||
|
|
items []JunkItem
|
|||
|
|
size int64
|
|||
|
|
}
|
|||
|
|
var groups []typeGroup
|
|||
|
|
for junkType, items := range byType {
|
|||
|
|
var size int64
|
|||
|
|
for _, item := range items {
|
|||
|
|
size += item.Size
|
|||
|
|
}
|
|||
|
|
groups = append(groups, typeGroup{junkType: junkType, items: items, size: size})
|
|||
|
|
}
|
|||
|
|
sort.Slice(groups, func(i, j int) bool {
|
|||
|
|
return groups[i].size > groups[j].size
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
PrintSectionHeader("JUNK BY TYPE")
|
|||
|
|
for _, group := range groups {
|
|||
|
|
safe := ""
|
|||
|
|
if len(group.items) > 0 && group.items[0].SafeToClean {
|
|||
|
|
safe = colors.Green + " [cleanable]" + colors.Reset
|
|||
|
|
} else {
|
|||
|
|
safe = colors.Yellow + " [manual cleanup]" + colors.Reset
|
|||
|
|
}
|
|||
|
|
fmt.Printf("%s%s📁 %s%s%s (%d items, %s)\n", colors.Red, colors.Bold, group.junkType, safe, colors.Reset, len(group.items), formatter.FormatSize(group.size))
|
|||
|
|
for _, item := range group.items {
|
|||
|
|
rel, _ := filepath.Rel(userHome, item.Path)
|
|||
|
|
if strings.HasPrefix(item.Path, "/") && !strings.HasPrefix(item.Path, userHome) {
|
|||
|
|
rel = item.Path
|
|||
|
|
}
|
|||
|
|
fmt.Printf(" %s\n", rel)
|
|||
|
|
}
|
|||
|
|
fmt.Println()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fmt.Printf("%s%s💡 Tip:%s Run %s z junks clean %s to remove cleanable junk files.\n", colors.Cyan, colors.Bold, colors.Reset, colors.Green, colors.Reset)
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func cleanJunks(args []string) error {
|
|||
|
|
force := false
|
|||
|
|
for _, arg := range args {
|
|||
|
|
if arg == "--force" || arg == "-f" {
|
|||
|
|
force = true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
stats, err := scanForJunks()
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("error scanning: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cleanableItems := filterCleanable(stats.Items)
|
|||
|
|
|
|||
|
|
if len(cleanableItems) == 0 {
|
|||
|
|
fmt.Printf("%s✨ No cleanable junk files found!%s\n", colors.Green, colors.Reset)
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var totalCleanable int64
|
|||
|
|
for _, item := range cleanableItems {
|
|||
|
|
totalCleanable += item.Size
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
PrintBoxHeader("CLEAN JUNK FILES", colors.Red)
|
|||
|
|
fmt.Printf("%s%s🧹 Found %d cleanable items (%s)%s\n\n", colors.Yellow, colors.Bold, len(cleanableItems), formatter.FormatSize(totalCleanable), colors.Reset)
|
|||
|
|
|
|||
|
|
if !force {
|
|||
|
|
fmt.Printf("%s%s⚠️ This will permanently delete:%s\n", colors.Red, colors.Bold, colors.Reset)
|
|||
|
|
byType := groupByType(cleanableItems)
|
|||
|
|
for junkType, items := range byType {
|
|||
|
|
var size int64
|
|||
|
|
for _, item := range items {
|
|||
|
|
size += item.Size
|
|||
|
|
}
|
|||
|
|
fmt.Printf(" • %s (%d items, %s)\n", junkType, len(items), formatter.FormatSize(size))
|
|||
|
|
}
|
|||
|
|
fmt.Println()
|
|||
|
|
fmt.Printf("%s%sConfirmation required.%s Run with %s--force%s to proceed.\n", colors.Cyan, colors.Bold, colors.Reset, colors.Green, colors.Reset)
|
|||
|
|
fmt.Printf("%s%sExample:%s z junks clean --force\n", colors.Cyan, colors.Bold, colors.Reset)
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fmt.Printf("%s%s🗑️ Cleaning junk files...%s\n\n", colors.Yellow, colors.Bold, colors.Reset)
|
|||
|
|
|
|||
|
|
var cleaned int64
|
|||
|
|
var failed int
|
|||
|
|
for _, item := range cleanableItems {
|
|||
|
|
if err := os.RemoveAll(item.Path); err != nil {
|
|||
|
|
failed++
|
|||
|
|
fmt.Printf(" %s✖%s Failed: %s\n", colors.Red, colors.Reset, item.Path)
|
|||
|
|
} else {
|
|||
|
|
cleaned += item.Size
|
|||
|
|
rel, _ := filepath.Rel(userHome, item.Path)
|
|||
|
|
if !strings.HasPrefix(item.Path, userHome) {
|
|||
|
|
rel = item.Path
|
|||
|
|
}
|
|||
|
|
fmt.Printf(" %s✓%s Removed: %s (%s)\n", colors.Green, colors.Reset, rel, formatter.FormatSize(item.Size))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fmt.Println()
|
|||
|
|
PrintSectionHeader("CLEANUP SUMMARY")
|
|||
|
|
fmt.Printf("%s%s✅ Removed:%s %d items (%s)\n", colors.Green, colors.Bold, colors.Reset, len(cleanableItems)-failed, formatter.FormatSize(cleaned))
|
|||
|
|
if failed > 0 {
|
|||
|
|
fmt.Printf("%s%s⚠️ Failed:%s %d items\n", colors.Red, colors.Bold, colors.Reset, failed)
|
|||
|
|
}
|
|||
|
|
fmt.Printf("%s%s💾 Space reclaimed:%s %s\n", colors.Cyan, colors.Bold, colors.Reset, formatter.FormatSize(cleaned))
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func scanForJunks() (*JunkStats, error) {
|
|||
|
|
stats := &JunkStats{
|
|||
|
|
Items: make([]JunkItem, 0),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
searchRoots := getJunkSearchRoots()
|
|||
|
|
|
|||
|
|
typeResult := scanDirs(searchRoots, junkPatterns, stats)
|
|||
|
|
|
|||
|
|
stats.TotalSize = typeResult.TotalSize
|
|||
|
|
stats.Cleanable = typeResult.Cleanable
|
|||
|
|
stats.NonCleanable = typeResult.NonCleanable
|
|||
|
|
|
|||
|
|
return stats, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func getJunkSearchRoots() []string {
|
|||
|
|
var roots []string
|
|||
|
|
|
|||
|
|
stdCache := os.Getenv("XDG_CACHE_HOME")
|
|||
|
|
if stdCache != "" {
|
|||
|
|
roots = append(roots, stdCache)
|
|||
|
|
} else {
|
|||
|
|
roots = append(roots, filepath.Join(userHome, ".cache"))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if appCache := filepath.Join(userHome, ".npm"); dirExists(appCache) {
|
|||
|
|
roots = append(roots, appCache)
|
|||
|
|
}
|
|||
|
|
if appCache := filepath.Join(userHome, ".yarn"); dirExists(appCache) {
|
|||
|
|
roots = append(roots, appCache)
|
|||
|
|
}
|
|||
|
|
if appCache := filepath.Join(userHome, ".pnpm-store"); dirExists(appCache) {
|
|||
|
|
roots = append(roots, appCache)
|
|||
|
|
}
|
|||
|
|
if appCache := filepath.Join(userHome, ".gradle"); dirExists(appCache) {
|
|||
|
|
roots = append(roots, appCache)
|
|||
|
|
}
|
|||
|
|
if appCache := filepath.Join(userHome, ".cargo/registry/cache"); dirExists(appCache) {
|
|||
|
|
roots = append(roots, appCache)
|
|||
|
|
}
|
|||
|
|
if appCache := filepath.Join(userHome, ".cargo/registry/index"); dirExists(appCache) {
|
|||
|
|
roots = append(roots, appCache)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
tempDir := os.TempDir()
|
|||
|
|
if tempDir != "" && dirExists(tempDir) {
|
|||
|
|
roots = append(roots, tempDir)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return roots
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func dirExists(path string) bool {
|
|||
|
|
info, err := os.Stat(path)
|
|||
|
|
return err == nil && info.IsDir()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func scanDirs(roots []string, patterns []struct {
|
|||
|
|
Pattern string
|
|||
|
|
Type string
|
|||
|
|
SafeToClean bool
|
|||
|
|
}, stats *JunkStats) *JunkStats {
|
|||
|
|
for _, root := range roots {
|
|||
|
|
if _, err := os.Stat(root); err != nil {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|||
|
|
if err != nil {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
baseName := filepath.Base(path)
|
|||
|
|
|
|||
|
|
for _, jp := range patterns {
|
|||
|
|
matched := false
|
|||
|
|
|
|||
|
|
if jp.Pattern == baseName || baseName == jp.Pattern {
|
|||
|
|
matched = true
|
|||
|
|
} else if strings.Contains(path, jp.Pattern) {
|
|||
|
|
matched = true
|
|||
|
|
} else if matched, _ = regexp.MatchString("^"+jp.Pattern+"$", baseName); matched {
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if matched {
|
|||
|
|
size := getDirSize(path)
|
|||
|
|
stats.Items = append(stats.Items, JunkItem{
|
|||
|
|
Path: path,
|
|||
|
|
Size: size,
|
|||
|
|
Type: jp.Type,
|
|||
|
|
SafeToClean: jp.SafeToClean,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
if info.IsDir() {
|
|||
|
|
if jp.SafeToClean {
|
|||
|
|
stats.Cleanable += size
|
|||
|
|
} else {
|
|||
|
|
stats.NonCleanable += size
|
|||
|
|
}
|
|||
|
|
stats.TotalSize += size
|
|||
|
|
return filepath.SkipDir
|
|||
|
|
} else {
|
|||
|
|
if jp.SafeToClean {
|
|||
|
|
stats.Cleanable += size
|
|||
|
|
} else {
|
|||
|
|
stats.NonCleanable += size
|
|||
|
|
}
|
|||
|
|
stats.TotalSize += size
|
|||
|
|
}
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return stats
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func getDirSize(path string) int64 {
|
|||
|
|
var size int64
|
|||
|
|
|
|||
|
|
info, err := os.Stat(path)
|
|||
|
|
if err != nil {
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if !info.IsDir() {
|
|||
|
|
return info.Size()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
filepath.Walk(path, func(filePath string, fileInfo os.FileInfo, err error) error {
|
|||
|
|
if err != nil {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
if fileInfo.Mode().IsRegular() {
|
|||
|
|
size += fileInfo.Size()
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return size
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func groupByType(items []JunkItem) map[string][]JunkItem {
|
|||
|
|
result := make(map[string][]JunkItem)
|
|||
|
|
for _, item := range items {
|
|||
|
|
result[item.Type] = append(result[item.Type], item)
|
|||
|
|
}
|
|||
|
|
return result
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func filterCleanable(items []JunkItem) []JunkItem {
|
|||
|
|
var cleanable []JunkItem
|
|||
|
|
for _, item := range items {
|
|||
|
|
if item.SafeToClean {
|
|||
|
|
cleanable = append(cleanable, item)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return cleanable
|
|||
|
|
}
|