package httpapi import ( "context" "log" "sync" "git.azzamo.net/Michilis/Spanglish/photo-api/internal/store" ) // statConcurrency bounds the one-off Stat burst below; a gallery of ~70 // photos then costs a handful of round-trips against S3, once ever. const statConcurrency = 8 // fillPreviewSizes measures the preview objects of photos whose // preview_size_bytes is still unset and writes the result back, updating the // slice in place. // // The worker records the size when it generates the preview, so this only // ever fires for rows that predate the column (or arrived via `photo-api // sync`). It is a self-healing cache fill rather than a migration step: the // first gallery view after deploy pays for one Stat per photo, every view // after that reads the stored value. Errors are logged and swallowed — a // missing size costs the save sheet its row label, which is not worth // failing a gallery response over. func (s *Server) fillPreviewSizes(ctx context.Context, photos []store.Photo) { var ( wg sync.WaitGroup sem = make(chan struct{}, statConcurrency) mu sync.Mutex size = make(map[string]int64, len(photos)) ) for _, p := range photos { if p.PreviewKey == "" || p.PreviewSizeBytes > 0 { continue } wg.Add(1) go func(p store.Photo) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() n, err := s.storage.Stat(ctx, p.PreviewKey) if err != nil { log.Printf("preview size %s: %v", p.PreviewKey, err) return } mu.Lock() size[p.ID] = n mu.Unlock() }(p) } wg.Wait() if len(size) == 0 { return } for i := range photos { if n, ok := size[photos[i].ID]; ok { photos[i].PreviewSizeBytes = n if err := s.db.SetPreviewSize(ctx, photos[i].ID, n); err != nil { log.Printf("store preview size %s: %v", photos[i].ID, err) } } } }