Commit 3004275

Eric Bower  ·  2026-05-10 20:54:24 -0400 EDT
parent d634672
chore: script to find orphaned buckets
1 files changed,  +114, -0
A cmd/scripts/clean-buckets/main.go
+114, -0
  1@@ -0,0 +1,114 @@
  2+package main
  3+
  4+import (
  5+	"flag"
  6+	"fmt"
  7+	"log/slog"
  8+	"os"
  9+	"strings"
 10+
 11+	"github.com/picosh/pico/pkg/db/postgres"
 12+	"github.com/picosh/pico/pkg/shared"
 13+	"github.com/picosh/pico/pkg/storage"
 14+)
 15+
 16+func main() {
 17+	deleteFlag := flag.Bool("delete", false, "delete orphaned bucket folders")
 18+	flag.Parse()
 19+
 20+	logger := slog.Default()
 21+
 22+	dbURL := shared.GetEnv("DATABASE_URL", "")
 23+	if dbURL == "" {
 24+		logger.Error("DATABASE_URL is required")
 25+		os.Exit(1)
 26+	}
 27+
 28+	dbpool := postgres.NewDB(dbURL, logger)
 29+	defer func() { _ = dbpool.Close() }()
 30+
 31+	st, err := storage.NewStorage(logger, "fs")
 32+	if err != nil {
 33+		logger.Error("failed to create storage", "err", err)
 34+		os.Exit(1)
 35+	}
 36+
 37+	// Collect all valid user IDs from the database
 38+	logger.Info("fetching all users from database")
 39+	users, err := dbpool.FindUsers()
 40+	if err != nil {
 41+		logger.Error("failed to fetch users", "err", err)
 42+		os.Exit(1)
 43+	}
 44+
 45+	validUserIDs := make(map[string]struct{}, len(users))
 46+	for _, user := range users {
 47+		validUserIDs[user.ID] = struct{}{}
 48+		logger.Info("found user", "id", user.ID, "name", user.Name)
 49+	}
 50+	logger.Info("total users", "count", len(validUserIDs))
 51+
 52+	// List all buckets in the storage directory
 53+	logger.Info("listing all buckets in storage")
 54+	buckets, err := st.ListBuckets()
 55+	if err != nil {
 56+		logger.Error("failed to list buckets", "err", err)
 57+		os.Exit(1)
 58+	}
 59+	logger.Info("total buckets", "count", len(buckets))
 60+
 61+	// Find orphaned buckets (no associated user)
 62+	orphaned := []string{}
 63+	for _, bucket := range buckets {
 64+		// Buckets are either the user ID directly (e.g., images)
 65+		// or prefixed with "static-" (e.g., static-{userID} for assets)
 66+		userID := bucket
 67+		if strings.HasPrefix(bucket, "static-") {
 68+			userID = strings.TrimPrefix(bucket, "static-")
 69+		}
 70+
 71+		if _, exists := validUserIDs[userID]; !exists {
 72+			orphaned = append(orphaned, bucket)
 73+		}
 74+	}
 75+
 76+	if len(orphaned) == 0 {
 77+		logger.Info("no orphaned buckets found")
 78+		return
 79+	}
 80+
 81+	logger.Info("orphaned buckets found", "count", len(orphaned))
 82+	for _, bucket := range orphaned {
 83+		fmt.Printf("  - %s\n", bucket)
 84+	}
 85+
 86+	if !*deleteFlag {
 87+		fmt.Printf("\nFound %d orphaned bucket(s). Use --delete to remove them.\n", len(orphaned))
 88+		return
 89+	}
 90+
 91+	// Delete orphaned buckets
 92+	logger.Info("deleting orphaned buckets")
 93+	deleted := 0
 94+	failed := 0
 95+	for _, bucket := range orphaned {
 96+		b, err := st.GetBucket(bucket)
 97+		if err != nil {
 98+			logger.Error("failed to get bucket", "bucket", bucket, "err", err)
 99+			failed++
100+			continue
101+		}
102+
103+		err = st.DeleteBucket(b)
104+		if err != nil {
105+			logger.Error("failed to delete bucket", "bucket", bucket, "err", err)
106+			failed++
107+			continue
108+		}
109+
110+		logger.Info("deleted bucket", "bucket", bucket)
111+		deleted++
112+	}
113+
114+	fmt.Printf("\nDeleted %d bucket(s), %d failed\n", deleted, failed)
115+}