Files

153 lines
4.1 KiB
Go

// Command uploadrewards is a one-off admin tool that uploads the sādhana
// reward images to Cloudflare R2 and writes the resulting public URLs back
// into rewards.json (and the embedded copy the BFF seeds from).
//
// Usage (from the spsays-bff directory, with R2_* in .env):
//
// go run ./cmd/uploadrewards
// go run ./cmd/uploadrewards -json ../sadhna-rewards/rewards.json
//
// It reads each reward's `file` from the images directory, uploads it to
// `<r2_prefix>/<file>` in the configured bucket, and sets `image_url` to the
// public URL. Re-running it is safe (objects are overwritten in place).
package main
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/joho/godotenv"
"github.com/prabhupadasays/spsays-bff/internal/repository"
)
type rewardEntry struct {
Key string `json:"key"`
File string `json:"file"`
Name string `json:"name"`
Meaning string `json:"meaning"`
Active bool `json:"active"`
ImageURL string `json:"image_url"`
}
type catalog struct {
Note string `json:"_note,omitempty"`
R2Prefix string `json:"r2_prefix"`
Rewards []rewardEntry `json:"rewards"`
}
func main() {
jsonPath := flag.String("json", "../sadhna-rewards/rewards.json", "path to rewards.json")
embedPath := flag.String("embed", "internal/repository/sadhna_rewards.json", "path to the BFF embed copy to keep in sync")
envPath := flag.String("env", ".env", "path to .env with R2_* vars")
dir := flag.String("dir", "", "images directory (default: directory of -json)")
flag.Parse()
_ = godotenv.Load(*envPath)
r2cfg := repository.R2Config{
AccountID: os.Getenv("R2_ACCOUNT_ID"),
AccessKeyID: os.Getenv("R2_ACCESS_KEY_ID"),
SecretAccessKey: os.Getenv("R2_SECRET_ACCESS_KEY"),
Bucket: os.Getenv("R2_BUCKET"),
PublicURL: os.Getenv("R2_PUBLIC_URL"),
}
r2, err := repository.NewR2(r2cfg)
if err != nil {
log.Fatalf("R2 init: %v", err)
}
if r2 == nil {
log.Fatalf("R2 not configured — set R2_* in %s", *envPath)
}
raw, err := os.ReadFile(*jsonPath)
if err != nil {
log.Fatalf("read %s: %v", *jsonPath, err)
}
var cat catalog
if err := json.Unmarshal(raw, &cat); err != nil {
log.Fatalf("parse %s: %v", *jsonPath, err)
}
imagesDir := *dir
if imagesDir == "" {
imagesDir = filepath.Dir(*jsonPath)
}
prefix := strings.Trim(cat.R2Prefix, "/")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
for i := range cat.Rewards {
rw := &cat.Rewards[i]
if rw.File == "" {
log.Printf("skip %q: no file", rw.Key)
continue
}
body, err := os.ReadFile(filepath.Join(imagesDir, rw.File))
if err != nil {
log.Fatalf("read image %s: %v", rw.File, err)
}
key := rw.File
if prefix != "" {
key = prefix + "/" + rw.File
}
url, err := r2.PutObject(ctx, key, contentType(rw.File), body)
if err != nil {
log.Fatalf("upload %s: %v", rw.File, err)
}
rw.ImageURL = url
fmt.Printf("✓ %-32s → %s\n", rw.Key, url)
}
out, err := marshalPretty(cat)
if err != nil {
log.Fatalf("encode json: %v", err)
}
if err := os.WriteFile(*jsonPath, out, 0o644); err != nil {
log.Fatalf("write %s: %v", *jsonPath, err)
}
if *embedPath != "" {
if err := os.WriteFile(*embedPath, out, 0o644); err != nil {
log.Fatalf("write embed copy %s: %v", *embedPath, err)
}
fmt.Printf("\nSynced embed copy: %s\n", *embedPath)
}
fmt.Printf("Uploaded %d reward image(s). image_url written to %s\n", len(cat.Rewards), *jsonPath)
}
func contentType(file string) string {
switch strings.ToLower(filepath.Ext(file)) {
case ".png":
return "image/png"
case ".svg":
return "image/svg+xml"
case ".jpg", ".jpeg":
return "image/jpeg"
case ".webp":
return "image/webp"
case ".gif":
return "image/gif"
default:
return "application/octet-stream"
}
}
func marshalPretty(v any) ([]byte, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false) // keep diacritics / punctuation literal
enc.SetIndent("", " ")
if err := enc.Encode(v); err != nil {
return nil, err
}
return buf.Bytes(), nil
}