From 344a6f6415c3c1b593677adec3b8844e0839971b Mon Sep 17 00:00:00 2001 From: s Date: Thu, 13 Nov 2025 14:43:15 -0500 Subject: created pretty printing for all commands --- internal/formatter/buckets.go | 470 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 470 insertions(+) create mode 100644 internal/formatter/buckets.go (limited to 'internal/formatter/buckets.go') diff --git a/internal/formatter/buckets.go b/internal/formatter/buckets.go new file mode 100644 index 0000000..9672b9d --- /dev/null +++ b/internal/formatter/buckets.go @@ -0,0 +1,470 @@ +package formatter + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + + "git.db.org.ai/dborg/internal/models" + "git.db.org.ai/dborg/internal/utils" +) + +func FormatBucketsResults(response *models.BucketsSearchResponse, asJSON bool) error { + if asJSON { + return utils.PrintJSON(response) + } + + PrintSection("Bucket Search Results") + + if response.Credits.Unlimited { + fmt.Printf("%s: %s\n", Dim("Credits"), Green("Unlimited")) + } else { + fmt.Printf("%s: %s\n", Dim("Credits Remaining"), FormatCredits(int64(response.Credits.Remaining))) + } + + if response.Results == nil { + PrintWarning("No results found") + return nil + } + + results, ok := response.Results.(map[string]any) + if !ok { + return fmt.Errorf("unexpected results format") + } + + buckets, ok := results["buckets"].([]any) + if !ok || len(buckets) == 0 { + PrintWarning("No buckets found") + return nil + } + + fmt.Printf("%s: %s\n\n", Dim("Total Buckets"), Yellow(fmt.Sprintf("%d", len(buckets)))) + + bucketGroups := groupBucketsByType(buckets) + + for _, bType := range getOrderedTypes(bucketGroups) { + formatBucketGroup(bType, bucketGroups[bType]) + } + + printBucketStats(buckets) + + return nil +} + +func groupBucketsByType(buckets []any) map[string][]map[string]any { + groups := make(map[string][]map[string]any) + + for _, bucket := range buckets { + if bucketMap, ok := bucket.(map[string]any); ok { + bucketType := "unknown" + if bt, ok := bucketMap["type"].(string); ok { + bucketType = bt + } + groups[bucketType] = append(groups[bucketType], bucketMap) + } + } + + return groups +} + +func getOrderedTypes(groups map[string][]map[string]any) []string { + var types []string + for t := range groups { + types = append(types, t) + } + + sort.Slice(types, func(i, j int) bool { + order := map[string]int{"aws": 1, "gcp": 2, "azure": 3, "dos": 4, "unknown": 99} + iOrder, iOk := order[types[i]] + jOrder, jOk := order[types[j]] + if !iOk { + iOrder = 50 + } + if !jOk { + jOrder = 50 + } + if iOrder != jOrder { + return iOrder < jOrder + } + return len(groups[types[i]]) > len(groups[types[j]]) + }) + + return types +} + +func formatBucketGroup(bucketType string, buckets []map[string]any) { + typeHeader := getBucketTypeHeader(bucketType) + fmt.Printf("%s %s\n", typeHeader, Gray(fmt.Sprintf("(%d)", len(buckets)))) + fmt.Println(Dim(strings.Repeat("─", 60))) + + sort.Slice(buckets, func(i, j int) bool { + iCount := getFileCount(buckets[i]) + jCount := getFileCount(buckets[j]) + return iCount > jCount + }) + + for i, bucket := range buckets { + formatSingleBucket(bucket, i+1) + } + + fmt.Println() +} + +func formatSingleBucket(bucket map[string]any, index int) { + bucketName := "unknown" + if name, ok := bucket["bucket"].(string); ok { + bucketName = name + } + + fileCount := getFileCount(bucket) + id := getID(bucket) + + fmt.Printf(" %s %s\n", Gray(fmt.Sprintf("%d.", index)), truncateBucketName(bucketName)) + + if fileCount > 0 { + fileCountStr := formatFileCount(fileCount) + fmt.Printf(" %s: %s", Cyan("Files"), fileCountStr) + } else { + fmt.Printf(" %s: %s", Cyan("Files"), Gray("Empty")) + } + + if id > 0 { + fmt.Printf(" %s %s", Dim("•"), Gray(fmt.Sprintf("ID: %d", id))) + } + + fmt.Println() +} + +func truncateBucketName(name string) string { + maxLen := 50 + if len(name) <= maxLen { + return Bold(name) + } + + parts := strings.Split(name, ".") + if len(parts) > 2 { + provider := parts[len(parts)-2] + "." + parts[len(parts)-1] + remaining := maxLen - len(provider) - 4 + if remaining > 0 { + return Bold(TruncateString(strings.Join(parts[:len(parts)-2], "."), remaining) + "..." + provider) + } + } + + return Bold(TruncateString(name, maxLen)) +} + +func formatFileCount(count int) string { + switch { + case count > 10000: + return Red(fmt.Sprintf("%s", formatNumber(count))) + case count > 1000: + return Yellow(fmt.Sprintf("%s", formatNumber(count))) + case count > 100: + return Green(fmt.Sprintf("%s", formatNumber(count))) + default: + return Cyan(fmt.Sprintf("%d", count)) + } +} + +func formatNumber(n int) string { + if n < 1000 { + return fmt.Sprintf("%d", n) + } + + str := fmt.Sprintf("%d", n) + var result strings.Builder + for i, r := range str { + if i > 0 && (len(str)-i)%3 == 0 { + result.WriteString(",") + } + result.WriteRune(r) + } + return result.String() +} + +func getBucketTypeHeader(bucketType string) string { + headers := map[string]string{ + "aws": Bold(Yellow("☁ AWS S3 Buckets")), + "gcp": Bold(Blue("☁ Google Cloud Storage")), + "azure": Bold(Cyan("☁ Azure Storage")), + "dos": Bold(Green("☁ DigitalOcean Spaces")), + } + + if header, ok := headers[bucketType]; ok { + return header + } + return Bold(Gray("☁ " + strings.Title(bucketType) + " Buckets")) +} + +func getFileCount(bucket map[string]any) int { + if count, ok := bucket["fileCount"].(float64); ok { + return int(count) + } + return 0 +} + +func getID(bucket map[string]any) int { + if id, ok := bucket["id"].(float64); ok { + return int(id) + } + return 0 +} + +func printBucketStats(buckets []any) { + totalFiles := 0 + emptyBuckets := 0 + largeBuckets := 0 + + typeCounts := make(map[string]int) + + for _, bucket := range buckets { + if bucketMap, ok := bucket.(map[string]any); ok { + fileCount := getFileCount(bucketMap) + totalFiles += fileCount + + if fileCount == 0 { + emptyBuckets++ + } else if fileCount > 1000 { + largeBuckets++ + } + + if bucketType, ok := bucketMap["type"].(string); ok { + typeCounts[bucketType]++ + } + } + } + + fmt.Printf("%s\n", Bold("Summary Statistics")) + fmt.Println(Dim(strings.Repeat("─", 60))) + + fmt.Printf(" %s: %s\n", Cyan("Total Files"), Yellow(formatNumber(totalFiles))) + fmt.Printf(" %s: %s\n", Cyan("Average Files/Bucket"), + Yellow(fmt.Sprintf("%.1f", float64(totalFiles)/float64(len(buckets))))) + + if emptyBuckets > 0 { + fmt.Printf(" %s: %s\n", Cyan("Empty Buckets"), Gray(fmt.Sprintf("%d", emptyBuckets))) + } + + if largeBuckets > 0 { + fmt.Printf(" %s: %s\n", Cyan("Large Buckets (>1000 files)"), + Red(fmt.Sprintf("%d", largeBuckets))) + } + + fmt.Println() +} + +func FormatBucketFilesResults(response *models.BucketsFilesSearchResponse, asJSON bool) error { + if asJSON { + return utils.PrintJSON(response) + } + + PrintSection("Bucket Files Search Results") + + if response.Credits.Unlimited { + fmt.Printf("%s: %s\n", Dim("Credits"), Green("Unlimited")) + } else { + fmt.Printf("%s: %s\n", Dim("Credits Remaining"), FormatCredits(int64(response.Credits.Remaining))) + } + + if response.Results == nil { + PrintWarning("No results found") + return nil + } + + results, ok := response.Results.(map[string]any) + if !ok { + resultsJSON, err := json.MarshalIndent(response.Results, "", " ") + if err != nil { + return fmt.Errorf("failed to format results: %w", err) + } + fmt.Println(string(resultsJSON)) + return nil + } + + files, ok := results["files"].([]any) + if !ok || len(files) == 0 { + PrintWarning("No files found") + return nil + } + + fmt.Printf("%s: %s\n\n", Dim("Total Files"), Yellow(fmt.Sprintf("%d", len(files)))) + + fileGroups := groupFilesByBucket(files) + + for bucket, bucketFiles := range fileGroups { + formatFileGroup(bucket, bucketFiles) + } + + return nil +} + +func groupFilesByBucket(files []any) map[string][]map[string]any { + groups := make(map[string][]map[string]any) + + for _, file := range files { + if fileMap, ok := file.(map[string]any); ok { + bucket := "unknown" + if b, ok := fileMap["bucket"].(string); ok && b != "" { + bucket = b + } else if url, ok := fileMap["url"].(string); ok && url != "" { + bucket = extractBucketFromURL(url) + } + groups[bucket] = append(groups[bucket], fileMap) + } + } + + return groups +} + +func extractBucketFromURL(url string) string { + url = strings.TrimPrefix(url, "https://") + url = strings.TrimPrefix(url, "http://") + + parts := strings.Split(url, "/") + if len(parts) > 0 { + return parts[0] + } + + return "unknown" +} + +func formatFileGroup(bucket string, files []map[string]any) { + fmt.Printf("%s %s\n", Bold(truncateBucketName(bucket)), Gray(fmt.Sprintf("(%d files)", len(files)))) + fmt.Println(Dim(strings.Repeat("─", 60))) + + for i, file := range files { + formatSingleFile(file, i+1) + } + + fmt.Println() +} + +func formatSingleFile(file map[string]any, index int) { + fileName := "unknown" + url := "" + + if name, ok := file["file"].(string); ok && name != "" { + fileName = name + } else if u, ok := file["url"].(string); ok && u != "" { + url = u + fileName = extractFileNameFromURL(u) + } + + fmt.Printf(" %s %s\n", Gray(fmt.Sprintf("%d.", index)), formatFileName(fileName)) + + if url != "" { + fmt.Printf(" %s: %s\n", Cyan("URL"), Dim(url)) + } + + if size, ok := file["size"].(float64); ok && size > 0 { + fmt.Printf(" %s: %s\n", Cyan("Size"), formatFileSize(int64(size))) + } + + if modified, ok := file["lastModified"].(string); ok && modified != "" { + fmt.Printf(" %s: %s\n", Cyan("Modified"), Dim(modified)) + } +} + +func extractFileNameFromURL(url string) string { + parts := strings.Split(url, "/") + if len(parts) > 0 { + fileName := parts[len(parts)-1] + + if qIndex := strings.Index(fileName, "?"); qIndex != -1 { + fileName = fileName[:qIndex] + } + + if hIndex := strings.Index(fileName, "#"); hIndex != -1 { + fileName = fileName[:hIndex] + } + + if fileName == "" { + return "index" + } + + return fileName + } + return "unknown" +} + +func formatFileName(name string) string { + if name == "" || name == "unknown" { + return Gray("(unnamed file)") + } + + parts := strings.Split(name, "/") + if len(parts) > 1 { + path := strings.Join(parts[:len(parts)-1], "/") + file := parts[len(parts)-1] + + if file == "" { + file = "(directory)" + } + + file = decodeURLEncoding(file) + + ext := getFileExtension(file) + color := getExtensionColor(ext) + + return Gray(path+"/") + Colorize(file, color) + } + + name = decodeURLEncoding(name) + ext := getFileExtension(name) + color := getExtensionColor(ext) + + return Colorize(name, color) +} + +func decodeURLEncoding(s string) string { + decoded := strings.ReplaceAll(s, "%20", " ") + decoded = strings.ReplaceAll(decoded, "%28", "(") + decoded = strings.ReplaceAll(decoded, "%29", ")") + decoded = strings.ReplaceAll(decoded, "_", " ") + + return decoded +} + +func getFileExtension(filename string) string { + parts := strings.Split(filename, ".") + if len(parts) > 1 { + return strings.ToLower(parts[len(parts)-1]) + } + return "" +} + +func getExtensionColor(ext string) string { + switch ext { + case "pdf", "doc", "docx", "txt": + return ColorBlue + case "jpg", "jpeg", "png", "gif", "svg": + return ColorGreen + case "mp4", "avi", "mov", "mkv": + return ColorMagenta + case "zip", "tar", "gz", "rar": + return ColorYellow + case "sql", "db", "sqlite": + return ColorRed + case "json", "xml", "yaml", "yml": + return ColorCyan + default: + return ColorWhite + } +} + +func formatFileSize(bytes int64) string { + color := ColorWhite + switch { + case bytes > 1024*1024*100: + color = ColorRed + case bytes > 1024*1024*10: + color = ColorYellow + case bytes > 1024*1024: + color = ColorGreen + default: + color = ColorCyan + } + + return Colorize(FormatBytes(bytes), color) +} -- cgit v1.2.3