Help us shape the future of Go! Take the 2026 Go Developer Survey

files

package
v1.49.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 27, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var FilesCmd = &cobra.Command{
	Use:   "files",
	Short: "A command for managing files",
	Long:  `This command will help manage various aspects of file operations on MacOS and Linux systems.`,
	Run: func(cmd *cobra.Command, _args []string) {
		err := cmd.Help()
		cobra.CheckErr(err)
	},
}
View Source
var FindAndDeleteCmd = &cobra.Command{
	Use:     "findAndDelete [directory]",
	Aliases: []string{"find-and-delete", "delete-files", "clean-files"},
	Short:   "Find and delete files of selected types, or list extensions",
	Long: `Recursively scan the provided directory for files of types selected by the user
and delete them after an interactive confirmation. Use --list-extensions to list
all file extensions in the directory instead. Use --filename to target a specific
filename, --glob for glob patterns, or --ext for file extensions.
`,
	Args: cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		headerStyle := lipgloss.NewStyle().
			Bold(true).
			Foreground(theme.Primary).
			MarginBottom(1)
		if !ui.DisableProgress {
			fmt.Fprintln(log.Out, headerStyle.Render("🧹 Find & Delete Files"))
		}

		dir := args[0]
		isVerbose := cmdutil.IsVerbose(cmd)

		if _, err := os.Stat(dir); os.IsNotExist(err) {
			log.Error("Provided directory does not exist: %s", dir)
			return
		}

		if listExtensions {
			extensions, err := ListExtensions(dir)
			if err != nil {
				log.Error("Error listing extensions: %v", err)
				return
			}

			var extLines []string
			extLines = append(extLines, fmt.Sprintf("Found %s file extension(s) in %s:",
				theme.PrimaryText.Bold(true).Render(fmt.Sprintf("%d", len(extensions))),
				theme.BoldText.Render(dir),
			))
			for _, ext := range extensions {
				extLines = append(extLines, fmt.Sprintf("  • %s", theme.PrimaryText.Render(ext)))
			}

			if !ui.DisableProgress {
				fmt.Println(theme.InfoBox.Render(strings.Join(extLines, "\n")))
			} else {
				log.Message("File extensions found in %s:", dir)
				for _, ext := range extensions {
					log.Message("  - %s", ext)
				}
			}
			return
		}

		matchFn, err := buildMatchFunction(globPattern, extension, filename)
		if err != nil {
			log.Error("Error building match function: %v", err)
			return
		}
		if matchFn == nil {
			options := []string{
				"JSON files (.json)",
				"Video files (.mp4, .mov, .avi, .mkv, .m4v, .wmv, .3gp)",
				"Image files (.jpg, .jpeg, .png, .gif)",
				"Microsoft documents (.doc, .docx, .xls, .xlsx, .ppt, .pptx)",
				"Archive files (.zip, .rar, .7z, .tar, .gz, .bz2)",
				"Audio files (.mp3, .wav, .flac, .aac, .ogg)",
				"PDF documents (.pdf)",
				"Text files (.txt, .md, .rtf)",
				"Log files (.log)",
				"Temporary files (.tmp, .temp, .swp)",
				"Backup files (.bak, .backup, .old)",
				"Executable files (.exe, .msi, .dmg, .pkg, .deb, .rpm)",
				"System files (.DS_Store)",
			}
			selected, err := ui.MultiSelect("Select file types to find and delete:", options, nil)
			if err != nil {
				log.Error("Error collecting selection: %v", err)
				return
			}

			if len(selected) == 0 {
				log.Info("No file types selected. Nothing to do.")
				return
			}

			extLookup := map[string]bool{}
			for _, s := range selected {
				s = strings.ToLower(s)
				if strings.Contains(s, "json") {
					extLookup[".json"] = true
				}
				if strings.Contains(s, "video") {
					extLookup[".mp4"] = true
					extLookup[".mov"] = true
					extLookup[".avi"] = true
					extLookup[".mkv"] = true
					extLookup[".m4v"] = true
					extLookup[".wmv"] = true
					extLookup[".3gp"] = true
				}
				if strings.Contains(s, "image") {
					extLookup[".jpg"] = true
					extLookup[".jpeg"] = true
					extLookup[".png"] = true
					extLookup[".gif"] = true
				}
				if strings.Contains(s, "microsoft") {
					extLookup[".doc"] = true
					extLookup[".docx"] = true
					extLookup[".xls"] = true
					extLookup[".xlsx"] = true
					extLookup[".ppt"] = true
					extLookup[".pptx"] = true
				}
				if strings.Contains(s, "archive") {
					extLookup[".zip"] = true
					extLookup[".rar"] = true
					extLookup[".7z"] = true
					extLookup[".tar"] = true
					extLookup[".gz"] = true
					extLookup[".bz2"] = true
				}
				if strings.Contains(s, "audio") {
					extLookup[".mp3"] = true
					extLookup[".wav"] = true
					extLookup[".flac"] = true
					extLookup[".aac"] = true
					extLookup[".ogg"] = true
				}
				if strings.Contains(s, "pdf") {
					extLookup[".pdf"] = true
				}
				if strings.Contains(s, "text") {
					extLookup[".txt"] = true
					extLookup[".md"] = true
					extLookup[".rtf"] = true
				}
				if strings.Contains(s, "log") {
					extLookup[".log"] = true
				}
				if strings.Contains(s, "temporary") || strings.Contains(s, "temp") {
					extLookup[".tmp"] = true
					extLookup[".temp"] = true
					extLookup[".swp"] = true
				}
				if strings.Contains(s, "backup") {
					extLookup[".bak"] = true
					extLookup[".backup"] = true
					extLookup[".old"] = true
				}
				if strings.Contains(s, "executable") {
					extLookup[".exe"] = true
					extLookup[".msi"] = true
					extLookup[".dmg"] = true
					extLookup[".pkg"] = true
					extLookup[".deb"] = true
					extLookup[".rpm"] = true
				}
				if strings.Contains(s, "system") {
					extLookup[".ds_store"] = true
				}
			}
			matchFn = func(name string) bool {
				return extLookup[strings.ToLower(filepath.Ext(name))]
			}
		}

		var spinner *ui.Spinner
		if !ui.DisableProgress {
			spinner = ui.NewProgressSpinner("Scanning directories for matching files...")
		}

		fileMatches, totalSize, walkErr := ScanFileMatches(dir, matchFn, spinner)
		if spinner != nil {
			spinner.Stop()
		}

		if walkErr != nil {
			log.Error("Error scanning directory: %v", walkErr)
			return
		}

		if len(fileMatches) == 0 {
			theme.SuccessMessage(fmt.Sprintf("No matching files found in %s.", dir))
			return
		}

		sizeFormatted := humanize.Bytes(uint64(totalSize))

		// Render Callout Box
		var boxLines []string
		boxLines = append(boxLines, fmt.Sprintf("Found %s matching file(s) reclaiming %s space in %s:",
			theme.PrimaryText.Bold(true).Render(fmt.Sprintf("%d", len(fileMatches))),
			theme.SuccessText.Bold(true).Render(sizeFormatted),
			theme.BoldText.Render(dir),
		))

		if !ui.DisableProgress {
			fmt.Println(theme.InfoBox.Render(strings.Join(boxLines, "\n")))
		}

		labelMap := make(map[string]FileMatch)
		var options []string
		for _, fm := range fileMatches {
			label := fm.FormatLabel()
			labelMap[label] = fm
			options = append(options, label)
		}

		confirmPromptMsg := fmt.Sprintf("Select files to delete (%s total matched):", sizeFormatted)
		selectedLabels, err := ui.MultiSelect(confirmPromptMsg, options, options)
		if err != nil {
			log.Error("Error during confirmation prompt: %v", err)
			return
		}

		if len(selectedLabels) == 0 {
			log.Info("Deletion canceled or no files selected.")
			return
		}

		var selectedFiles []FileMatch
		var selectedTotalBytes int64
		for _, label := range selectedLabels {
			if fm, ok := labelMap[label]; ok {
				selectedFiles = append(selectedFiles, fm)
				selectedTotalBytes += fm.SizeBytes
			}
		}

		totalToDelete := len(selectedFiles)
		var deleteSpinner *ui.Spinner
		if !ui.DisableProgress {
			deleteSpinner = ui.NewProgressSpinner(fmt.Sprintf("Deleting %d selected file(s)...", totalToDelete))
		}

		var deletedCount int
		var freedBytes int64
		var errorCount int

		for i, fm := range selectedFiles {
			ratio := float64(i+1) / float64(totalToDelete)
			statusMsg := fmt.Sprintf("[%d/%d] Deleting %s", i+1, totalToDelete, filepath.Base(fm.Path))

			if deleteSpinner != nil {
				deleteSpinner.SetProgressBar(ratio, statusMsg)
			}

			if err := os.Remove(fm.Path); err != nil && !os.IsNotExist(err) {
				errorCount++
				if deleteSpinner != nil {
					deleteSpinner.Logf("  %s Failed %s: %v\n", theme.ErrorText.Render("✗"), fm.Path, err)
				} else {
					log.Error("Failed to delete %s: %v", fm.Path, err)
				}
			} else {
				deletedCount++
				freedBytes += fm.SizeBytes
				sizeStr := ""
				if fm.SizeBytes > 0 {
					sizeStr = fmt.Sprintf(" (freed %s)", humanize.Bytes(uint64(fm.SizeBytes)))
				}
				if deleteSpinner != nil {
					deleteSpinner.Logf("  %s Deleted %s%s\n", theme.SuccessText.Render("✓"), fm.Path, sizeStr)
				} else {
					if isVerbose {
						log.Success("Deleted %s%s", fm.Path, sizeStr)
					}
				}
			}
		}

		if deleteSpinner != nil {
			deleteSpinner.SetProgressBar(1.0, "Deletion complete")
			deleteSpinner.Stop()
		}

		freedStr := humanize.Bytes(uint64(freedBytes))
		if errorCount > 0 {
			theme.WarningMessage(
				fmt.Sprintf(
					"Deleted %d file(s) freeing %s, but encountered %d error(s).",
					deletedCount,
					freedStr,
					errorCount,
				),
			)
		} else {
			theme.SuccessMessage(
				fmt.Sprintf("Successfully deleted %d file(s) freeing %s of disk space!", deletedCount, freedStr),
			)
		}
	},
}
View Source
var FindNonMovieFoldersCmd = &cobra.Command{
	Use:     "findNonMovieFolders [directory]",
	Aliases: []string{"find-non-movie-folders", "non-movie-folders", "clean-folders"},
	Short:   "Find and optionally delete non-movie folders",
	Long: `This command searches recursively through the supplied directory for directories
that do not contain video files (mp4, mkv, avi, mov, wmv, flv, webm, mpeg, mpg, m4v).
It identifies top-level subdirectories within the supplied directory that lack
any such files anywhere within their structure.

It calculates folder disk space, lists folder contents, and prompts for confirmation before deletion.`,
	Args: cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		headerStyle := lipgloss.NewStyle().
			Bold(true).
			Foreground(theme.Primary).
			MarginBottom(1)
		if !ui.DisableProgress {
			fmt.Fprintln(log.Out, headerStyle.Render("🎬 Find Non-Movie Folders"))
		}

		directory := args[0]
		isVerbose := cmdutil.IsVerbose(cmd)

		if _, err := os.Stat(directory); os.IsNotExist(err) {
			log.Error("Provided directory does not exist: %s", directory)
			return
		}

		var spinner *ui.Spinner
		if !ui.DisableProgress {
			spinner = ui.NewProgressSpinner("Scanning directories for non-movie folders...")
		}

		nonMovieFolders, err := findNonMovieFolders(isVerbose, directory, spinner, func(done, total int) {
			progress := 0.0
			if total > 0 {
				progress = float64(done) / float64(total)
			}
			if spinner != nil {
				spinner.SetProgressBar(progress, fmt.Sprintf("Scanning... (%d/%d directories)", done, total))
			}
		})

		if spinner != nil {
			spinner.Stop()
		}

		if err != nil {
			log.Error("Error finding non-movie folders: %s", err)
			return
		}

		if len(nonMovieFolders) == 0 {
			theme.SuccessMessage(fmt.Sprintf("No non-movie folders found in %s.", directory))
			return
		}

		// Inspect each non-movie folder for size and file count
		var folderMatches []FolderMatch
		var totalReclaimableBytes int64
		var totalFiles int

		for _, folder := range nonMovieFolders {
			sz := asdf.CalculateDirSize(folder)
			fc := countFilesInDir(folder)

			folderMatches = append(folderMatches, FolderMatch{
				Path:      folder,
				SizeBytes: sz,
				FileCount: fc,
			})

			totalReclaimableBytes += sz
			totalFiles += fc
		}

		totalSizeStr := humanize.Bytes(uint64(totalReclaimableBytes))

		// Render Callout Box
		var boxLines []string
		boxLines = append(boxLines, fmt.Sprintf("Found %s non-movie folder(s) reclaiming %s space (%d total file(s)):",
			theme.PrimaryText.Bold(true).Render(fmt.Sprintf("%d", len(folderMatches))),
			theme.SuccessText.Bold(true).Render(totalSizeStr),
			totalFiles,
		))

		for _, fm := range folderMatches {
			boxLines = append(boxLines, fmt.Sprintf(
				"  • %s %s",
				theme.BoldText.Render(fm.Path),
				theme.MutedText.Render(
					fmt.Sprintf("(%s, %d files)", humanize.Bytes(uint64(fm.SizeBytes)), fm.FileCount),
				),
			))
		}

		if !ui.DisableProgress {
			fmt.Println(theme.InfoBox.Render(strings.Join(boxLines, "\n")))
		}

		labelMap := make(map[string]FolderMatch)
		var options []string
		for _, fm := range folderMatches {
			label := fm.FormatLabel()
			labelMap[label] = fm
			options = append(options, label)
		}

		confirmPromptMsg := fmt.Sprintf("Select non-movie folders to delete (%s total reclaimable):", totalSizeStr)
		selectedLabels, err := ui.MultiSelect(confirmPromptMsg, options, options)
		if err != nil {
			log.Error("Error during confirmation prompt: %v", err)
			return
		}

		if len(selectedLabels) == 0 {
			log.Info("Deletion canceled or no folders selected.")
			return
		}

		var selectedFolders []FolderMatch
		for _, label := range selectedLabels {
			if fm, ok := labelMap[label]; ok {
				selectedFolders = append(selectedFolders, fm)
			}
		}

		totalToDelete := len(selectedFolders)
		var deleteSpinner *ui.Spinner
		if !ui.DisableProgress {
			deleteSpinner = ui.NewProgressSpinner(fmt.Sprintf("Deleting %d non-movie folder(s)...", totalToDelete))
		}

		var deletedCount int
		var freedBytes int64
		var errorCount int

		for i, fm := range selectedFolders {
			ratio := float64(i+1) / float64(totalToDelete)
			statusMsg := fmt.Sprintf("[%d/%d] Removing %s", i+1, totalToDelete, filepath.Base(fm.Path))

			if deleteSpinner != nil {
				deleteSpinner.SetProgressBar(ratio, statusMsg)
			}

			if err := os.RemoveAll(fm.Path); err != nil {
				errorCount++
				if deleteSpinner != nil {
					deleteSpinner.Logf("  %s Failed to remove %s: %v\n", theme.ErrorText.Render("✗"), fm.Path, err)
				} else {
					log.Error("Error deleting folder %s: %v", fm.Path, err)
				}
			} else {
				deletedCount++
				freedBytes += fm.SizeBytes
				sizeStr := humanize.Bytes(uint64(fm.SizeBytes))
				if deleteSpinner != nil {
					deleteSpinner.Logf(
						"  %s Deleted %s (freed %s, %d files)\n",
						theme.SuccessText.Render("✓"),
						fm.Path,
						sizeStr,
						fm.FileCount,
					)
				} else {
					log.Success("Deleted: %s (freed %s)", fm.Path, sizeStr)
				}
			}
		}

		if deleteSpinner != nil {
			deleteSpinner.SetProgressBar(1.0, "Folder deletion complete")
			deleteSpinner.Stop()
		}

		freedStr := humanize.Bytes(uint64(freedBytes))
		if errorCount > 0 {
			theme.WarningMessage(
				fmt.Sprintf(
					"Deleted %d non-movie folder(s) freeing %s, but encountered %d error(s).",
					deletedCount,
					freedStr,
					errorCount,
				),
			)
		} else {
			theme.SuccessMessage(
				fmt.Sprintf(
					"Successfully deleted %d non-movie folder(s) freeing %s of disk space!",
					deletedCount,
					freedStr,
				),
			)
		}
	},
}

FindNonMovieFoldersCmd defines the cobra command for finding and optionally deleting directories that do not contain common video file types.

Functions

func ListExtensions added in v0.25.16

func ListExtensions(dir string) ([]string, error)

func ScanFiles

func ScanFiles(dir string, matchFn func(name string) bool, spinner *ui.Spinner) ([]string, int64, error)

ScanFiles walks dir recursively and returns files that match the provided function. Maintained for backward compatibility.

Types

type FileMatch added in v1.23.0

type FileMatch struct {
	Path      string
	SizeBytes int64
}

FileMatch tracks a matched file path and its size in bytes.

func ScanFileMatches added in v1.23.0

func ScanFileMatches(dir string, matchFn func(name string) bool, spinner *ui.Spinner) ([]FileMatch, int64, error)

ScanFileMatches walks dir recursively and returns FileMatch structs containing path and size.

func (FileMatch) FormatLabel added in v1.23.0

func (f FileMatch) FormatLabel() string

FormatLabel returns a user-friendly display string with path and human-readable size.

type FileTypeCategory

type FileTypeCategory struct {
	Name       string
	Extensions map[string]bool
	Options    []string // For display in the survey
}

FileTypeCategory represents a category of file types with their extensions.

type FolderMatch added in v1.23.0

type FolderMatch struct {
	Path      string
	SizeBytes int64
	FileCount int
}

FolderMatch represents a detected non-movie folder with disk size and file count.

func (FolderMatch) FormatLabel added in v1.23.0

func (f FolderMatch) FormatLabel() string

FormatLabel returns a user-friendly display string with folder path, size, and file count.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL