174 lines
3.7 KiB
Go
174 lines
3.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/user/aevs/internal/config"
|
|
"github.com/user/aevs/internal/storage"
|
|
"github.com/user/aevs/internal/types"
|
|
)
|
|
|
|
var (
|
|
listJSON bool
|
|
)
|
|
|
|
var listCmd = &cobra.Command{
|
|
Use: "list",
|
|
Short: "List projects in storage",
|
|
Long: `Show all projects stored in cloud storage.`,
|
|
RunE: runList,
|
|
}
|
|
|
|
func init() {
|
|
listCmd.Flags().BoolVar(&listJSON, "json", false, "output in JSON format")
|
|
}
|
|
|
|
func runList(cmd *cobra.Command, args []string) error {
|
|
// Load global config
|
|
globalCfg, err := config.LoadGlobalConfig()
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return fmt.Errorf("no storage configured; run 'aevs config' first")
|
|
}
|
|
return fmt.Errorf("failed to load global config: %w", err)
|
|
}
|
|
|
|
// Create storage client
|
|
s3Storage, err := storage.NewS3Storage(&globalCfg.Storage)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create storage client: %w", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// List projects
|
|
projects, err := s3Storage.ListProjects(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list projects: %w", err)
|
|
}
|
|
|
|
if len(projects) == 0 {
|
|
fmt.Println("No projects found in storage.")
|
|
return nil
|
|
}
|
|
|
|
// Load metadata for each project
|
|
var projectInfos []types.ProjectInfo
|
|
for _, project := range projects {
|
|
metadataKey := fmt.Sprintf("%s/%s", project, config.MetadataFileName)
|
|
metadataReader, err := s3Storage.Download(ctx, metadataKey)
|
|
if err != nil {
|
|
// Skip projects without metadata
|
|
continue
|
|
}
|
|
|
|
metadataBytes, err := io.ReadAll(metadataReader)
|
|
metadataReader.Close()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
var metadata types.Metadata
|
|
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
|
continue
|
|
}
|
|
|
|
projectInfos = append(projectInfos, types.ProjectInfo{
|
|
Name: project,
|
|
FileCount: len(metadata.Files),
|
|
UpdatedAt: metadata.UpdatedAt,
|
|
SizeBytes: metadata.SizeBytes,
|
|
})
|
|
}
|
|
|
|
if listJSON {
|
|
// JSON output
|
|
output, err := json.MarshalIndent(projectInfos, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal JSON: %w", err)
|
|
}
|
|
fmt.Println(string(output))
|
|
return nil
|
|
}
|
|
|
|
// Table output
|
|
fmt.Println()
|
|
fmt.Printf("Projects in s3://%s:\n", globalCfg.Storage.Bucket)
|
|
fmt.Println()
|
|
fmt.Printf(" %-25s %-8s %-20s %s\n", "PROJECT", "FILES", "UPDATED", "SIZE")
|
|
|
|
for _, info := range projectInfos {
|
|
updatedStr := formatTime(info.UpdatedAt)
|
|
sizeStr := formatSize(info.SizeBytes)
|
|
fmt.Printf(" %-25s %-8d %-20s %s\n", info.Name, info.FileCount, updatedStr, sizeStr)
|
|
}
|
|
|
|
fmt.Println()
|
|
fmt.Printf("Total: %d project(s)\n", len(projectInfos))
|
|
|
|
return nil
|
|
}
|
|
|
|
// formatTime formats time in a human-readable way
|
|
func formatTime(t time.Time) string {
|
|
now := time.Now()
|
|
diff := now.Sub(t)
|
|
|
|
if diff < time.Minute {
|
|
return "just now"
|
|
}
|
|
if diff < time.Hour {
|
|
mins := int(diff.Minutes())
|
|
if mins == 1 {
|
|
return "1 minute ago"
|
|
}
|
|
return fmt.Sprintf("%d minutes ago", mins)
|
|
}
|
|
if diff < 24*time.Hour {
|
|
hours := int(diff.Hours())
|
|
if hours == 1 {
|
|
return "1 hour ago"
|
|
}
|
|
return fmt.Sprintf("%d hours ago", hours)
|
|
}
|
|
if diff < 7*24*time.Hour {
|
|
days := int(diff.Hours() / 24)
|
|
if days == 1 {
|
|
return "1 day ago"
|
|
}
|
|
return fmt.Sprintf("%d days ago", days)
|
|
}
|
|
if diff < 30*24*time.Hour {
|
|
weeks := int(diff.Hours() / 24 / 7)
|
|
if weeks == 1 {
|
|
return "1 week ago"
|
|
}
|
|
return fmt.Sprintf("%d weeks ago", weeks)
|
|
}
|
|
|
|
return t.Format("2006-01-02")
|
|
}
|
|
|
|
// formatSize formats bytes in a human-readable way
|
|
func formatSize(bytes int64) string {
|
|
const unit = 1024
|
|
if bytes < unit {
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
|
|
div, exp := int64(unit), 0
|
|
for n := bytes / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
|
|
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
}
|