feat: add web push and sadhna tracker
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user