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) }