Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
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) }, }
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.Fprintln(log.Out, 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.Fprintln(log.Out, 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), ) } }, }
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.Fprintln(log.Out, 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.
var ShredCmd = &cobra.Command{ Use: "shred [paths...]", Aliases: []string{"secure-delete", "srm"}, Short: "Securely delete files and directories by overwriting data", Long: `Securely delete files and directories by overwriting their data multiple times before removal. This prevents recovery of deleted data using forensic tools. Supports multiple overwrite methods including DoD 5220.22-M (3-pass) and Gutmann (35-pass). By default, uses a 3-pass random overwrite which is suitable for most use cases. Examples: eng files shred sensitive.txt # Shred a single file (3 passes) eng files shred -r secrets/ # Shred directory recursively eng files shred -p 7 -m dod file1 file2 # 7-pass DoD method eng files shred --dry-run secrets/ # Preview what would be shredded eng files shred -f -r /path/to/data # Force (no confirmation)`, Args: cobra.MinimumNArgs(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("🗑️ Secure File Deletion (shred)")) } isVerbose := cmdutil.IsVerbose(cmd) method := fs.ShredMethod(shredMethod) validMethods := map[fs.ShredMethod]bool{ fs.MethodAuto: true, fs.MethodRandom: true, fs.MethodZero: true, fs.MethodDoD: true, fs.MethodGutmann: true, } if !validMethods[method] { log.Error("Invalid method: %s. Valid: auto, random, zero, dod, gutmann", shredMethod) return } files, err := fs.CollectFilesForShredding(args, shredRecursive) if err != nil { log.Error("%v", err) return } if len(files) == 0 { theme.InfoMessage("No files to shred.") return } // Calculate total size and passes var totalSize int64 fileInfos := make([]FileMatch, 0, len(files)) for _, file := range files { info, err := os.Stat(file) size := int64(0) if err == nil { size = info.Size() } totalSize += size fileInfos = append(fileInfos, FileMatch{ Path: file, SizeBytes: size, }) } sort.Slice(fileInfos, func(i, j int) bool { return fileInfos[i].Path < fileInfos[j].Path }) passes := shredPasses if passes <= 0 { passes = 3 } totalPasses := fs.GetPassesForMethod(method, passes) sizeFormatted := humanize.Bytes(uint64(totalSize)) passDesc := fmt.Sprintf("%d pass", totalPasses) if totalPasses != 1 { passDesc += "es" } var boxLines []string boxLines = append(boxLines, fmt.Sprintf( "Ready to securely delete %s file(s) (%s) using %s (%s method):", theme.PrimaryText.Bold(true).Render(fmt.Sprintf("%d", len(files))), theme.SuccessText.Bold(true).Render(sizeFormatted), theme.PrimaryText.Bold(true).Render(passDesc), theme.PrimaryText.Bold(true).Render(string(method)), )) if shredDryRun { warningStyle := lipgloss.NewStyle(). Foreground(lipgloss.AdaptiveColor{Light: "#f59e0b", Dark: "#d97706"}). Bold(true) boxLines = append(boxLines, warningStyle.Render("DRY RUN - No files will be deleted")) } if !ui.DisableProgress { fmt.Fprintln(log.Out, theme.InfoBox.Render(strings.Join(boxLines, "\n"))) } else { log.Message("Files to shred:") for _, fm := range fileInfos { sizeStr := "" if fm.SizeBytes > 0 { sizeStr = fmt.Sprintf(" (%s)", humanize.Bytes(uint64(fm.SizeBytes))) } log.Message(" %s%s", fm.Path, sizeStr) } log.Message("Method: %s, Passes: %d", method, totalPasses) } if shredDryRun { theme.InfoMessage("Dry run complete. No files were deleted.") return } if !shredForce { confirmed, err := ui.Confirm( fmt.Sprintf("Permanently shred %d file(s) (%s)? This cannot be undone!", len(files), sizeFormatted), false, ) if err != nil { log.Error("Confirmation failed: %v", err) return } if !confirmed { log.Info("Shredding canceled.") return } } // Initialize multi-progress bar var multiBar *ui.MultiProgressBar var bars []*ui.ProgressBar if !ui.DisableProgress { multiBar = ui.NewMultiProgressBar(log.Writer()) multiBar.SetOverallMessage(fmt.Sprintf("Shredding %d file(s) with %d pass(es)...", len(files), totalPasses)) bars = make([]*ui.ProgressBar, len(fileInfos)) for i, fm := range fileInfos { label := fm.Path if len(label) > 50 { label = "..." + label[len(label)-47:] } bars[i] = multiBar.AddBar(label, totalPasses) } multiBar.SetOverallMessage(fmt.Sprintf("Shredding %d file(s) with %d pass(es)...", len(files), totalPasses)) } // Track results var deletedCount int var freedBytes int64 var errors []string progressFn := func(statuses []fs.FileShredStatus) { if multiBar == nil { return } for _, status := range statuses { for i, fm := range fileInfos { if fm.Path == status.Path { detail := "" if status.Size > 0 { detail = humanize.Bytes(uint64(status.Size)) } multiBar.UpdateBar(bars[i], status.Percent, detail, status.CurrentPass) if status.Done { if status.Error != nil { multiBar.CompleteBar(bars[i], status.Error) } else { multiBar.CompleteBar(bars[i], nil) } } break } } } } startTime := time.Now() err = fs.ShredMultiple(files, passes, method, progressFn) elapsed := time.Since(startTime).Round(time.Second) if multiBar != nil { multiBar.Stop() } if err != nil { errors = append(errors, err.Error()) theme.WarningMessage(fmt.Sprintf("Shredding completed with errors in %s", elapsed)) } else { deletedCount = len(files) freedBytes = totalSize freedStr := humanize.Bytes(uint64(freedBytes)) theme.SuccessMessage( fmt.Sprintf("Successfully shredded %d file(s) freeing %s in %s!", deletedCount, freedStr, elapsed), ) } if isVerbose && len(errors) > 0 { for _, e := range errors { log.Error("%s", e) } } }, }
ShredCmd represents the secure file deletion command.
Functions ¶
func ListExtensions ¶ added in v0.25.16
Types ¶
type FileMatch ¶ added in v1.23.0
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
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
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.