summaryrefslogtreecommitdiffstats
path: root/internal/formatter/buckets.go
diff options
context:
space:
mode:
authors <[email protected]>2025-11-13 14:43:15 -0500
committers <[email protected]>2025-11-13 14:43:15 -0500
commit344a6f6415c3c1b593677adec3b8844e0839971b (patch)
treeb05291ecdf21917b27e9e234eeb997c2706966d5 /internal/formatter/buckets.go
parenta5fc01a03753c9a18ddeaf13610dd99b4b311b80 (diff)
downloaddborg-344a6f6415c3c1b593677adec3b8844e0839971b.tar.gz
dborg-344a6f6415c3c1b593677adec3b8844e0839971b.zip
created pretty printing for all commandsv1.0.0
Diffstat (limited to 'internal/formatter/buckets.go')
-rw-r--r--internal/formatter/buckets.go470
1 files changed, 470 insertions, 0 deletions
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)
+}