main pico / cmd / scripts / clean-buckets / main.go
Eric Bower  ·  2026-05-10
  1package main
  2
  3import (
  4	"flag"
  5	"fmt"
  6	"log/slog"
  7	"os"
  8	"strings"
  9
 10	"github.com/picosh/pico/pkg/db/postgres"
 11	"github.com/picosh/pico/pkg/shared"
 12	"github.com/picosh/pico/pkg/storage"
 13)
 14
 15func main() {
 16	deleteFlag := flag.Bool("delete", false, "delete orphaned bucket folders")
 17	flag.Parse()
 18
 19	logger := slog.Default()
 20
 21	dbURL := shared.GetEnv("DATABASE_URL", "")
 22	if dbURL == "" {
 23		logger.Error("DATABASE_URL is required")
 24		os.Exit(1)
 25	}
 26
 27	dbpool := postgres.NewDB(dbURL, logger)
 28	defer func() { _ = dbpool.Close() }()
 29
 30	adapter := storage.GetStorageTypeFromEnv()
 31	st, err := storage.NewStorage(logger, adapter)
 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}