153 lines
4.1 KiB
Go
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
|
|
}
|