at main 6.7 kB view raw
1#!/usr/bin/env bash 2set -euo pipefail 3 4# Bunny CDN Deploy Script for Grain SPA 5# Builds the app and syncs dist/ to Bunny Storage 6 7# Colors for output 8RED='\033[0;31m' 9GREEN='\033[0;32m' 10YELLOW='\033[1;33m' 11NC='\033[0m' # No Color 12 13# Counters 14UPLOADED=0 15DELETED=0 16 17# Parse arguments 18DRY_RUN=false 19VERBOSE=false 20SKIP_BUILD=false 21 22while [[ $# -gt 0 ]]; do 23 case $1 in 24 --dry-run) 25 DRY_RUN=true 26 shift 27 ;; 28 --verbose|-v) 29 VERBOSE=true 30 shift 31 ;; 32 --skip-build) 33 SKIP_BUILD=true 34 shift 35 ;; 36 *) 37 echo -e "${RED}Unknown option: $1${NC}" 38 exit 1 39 ;; 40 esac 41done 42 43# Load .env file if it exists 44if [ -f .env ]; then 45 set -a 46 source .env 47 set +a 48fi 49 50# Required environment variables 51: "${BUNNY_API_KEY:?BUNNY_API_KEY environment variable is required}" 52: "${BUNNY_STORAGE_PASSWORD:?BUNNY_STORAGE_PASSWORD environment variable is required}" 53: "${BUNNY_STORAGE_ZONE:?BUNNY_STORAGE_ZONE environment variable is required}" 54: "${BUNNY_STORAGE_HOST:?BUNNY_STORAGE_HOST environment variable is required}" 55: "${BUNNY_PULLZONE_ID:?BUNNY_PULLZONE_ID environment variable is required}" 56 57# Configuration 58STORAGE_URL="https://${BUNNY_STORAGE_HOST}/${BUNNY_STORAGE_ZONE}" 59DIST_DIR="dist" 60 61echo "Bunny CDN Deploy - Grain SPA" 62echo "========================================" 63echo "Storage Zone: ${BUNNY_STORAGE_ZONE}" 64if [ "$DRY_RUN" = true ]; then 65 echo -e "${YELLOW}DRY RUN MODE - No changes will be made${NC}" 66fi 67echo "" 68 69# Get content type for file 70get_content_type() { 71 local file="$1" 72 case "$file" in 73 *.html) echo "text/html" ;; 74 *.js) echo "application/javascript" ;; 75 *.css) echo "text/css" ;; 76 *.json) echo "application/json" ;; 77 *.svg) echo "image/svg+xml" ;; 78 *.png) echo "image/png" ;; 79 *.jpg|*.jpeg) echo "image/jpeg" ;; 80 *.gif) echo "image/gif" ;; 81 *.webp) echo "image/webp" ;; 82 *.woff) echo "font/woff" ;; 83 *.woff2) echo "font/woff2" ;; 84 *.ttf) echo "font/ttf" ;; 85 *.ico) echo "image/x-icon" ;; 86 *) echo "application/octet-stream" ;; 87 esac 88} 89 90# Upload a single file 91upload_file() { 92 local local_path="$1" 93 local remote_path="$2" 94 95 if [ "$VERBOSE" = true ]; then 96 echo " Uploading: ${remote_path}" 97 fi 98 99 if [ "$DRY_RUN" = true ]; then 100 ((UPLOADED++)) 101 return 0 102 fi 103 104 local content_type 105 content_type=$(get_content_type "$local_path") 106 107 local http_code 108 http_code=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \ 109 "${STORAGE_URL}/${remote_path}" \ 110 -H "AccessKey: ${BUNNY_STORAGE_PASSWORD}" \ 111 -H "Content-Type: ${content_type}" \ 112 --data-binary "@${local_path}") 113 114 if [[ "$http_code" =~ ^2 ]]; then 115 ((UPLOADED++)) 116 return 0 117 else 118 echo -e "${RED}Failed to upload ${remote_path}: HTTP ${http_code}${NC}" 119 return 1 120 fi 121} 122 123# List all files in remote storage recursively 124list_remote_files() { 125 local path="${1:-}" 126 local response 127 response=$(curl -s -X GET "${STORAGE_URL}/${path}" \ 128 -H "AccessKey: ${BUNNY_STORAGE_PASSWORD}" \ 129 -H "Accept: application/json") 130 131 echo "$response" | jq -r '.[] | if .IsDirectory then .ObjectName + "/" else .ObjectName end' 2>/dev/null | while read -r item; do 132 if [[ "$item" == */ ]]; then 133 # It's a directory, recurse 134 list_remote_files "${path}${item}" 135 else 136 echo "${path}${item}" 137 fi 138 done 139} 140 141# Delete a single file from remote 142delete_file() { 143 local remote_path="$1" 144 145 if [ "$VERBOSE" = true ]; then 146 echo " Deleting: ${remote_path}" 147 fi 148 149 if [ "$DRY_RUN" = true ]; then 150 ((DELETED++)) 151 return 0 152 fi 153 154 local http_code 155 http_code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ 156 "${STORAGE_URL}/${remote_path}" \ 157 -H "AccessKey: ${BUNNY_STORAGE_PASSWORD}") 158 159 if [[ "$http_code" =~ ^2 ]]; then 160 ((DELETED++)) 161 return 0 162 else 163 echo -e "${RED}Failed to delete ${remote_path}: HTTP ${http_code}${NC}" 164 return 1 165 fi 166} 167 168# Purge pull zone cache 169purge_cache() { 170 echo "Purging CDN cache..." 171 172 if [ "$DRY_RUN" = true ]; then 173 echo -e "${YELLOW} Would purge pull zone ${BUNNY_PULLZONE_ID}${NC}" 174 return 0 175 fi 176 177 local http_code 178 http_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ 179 "https://api.bunny.net/pullzone/${BUNNY_PULLZONE_ID}/purgeCache" \ 180 -H "AccessKey: ${BUNNY_API_KEY}" \ 181 -H "Content-Type: application/json") 182 183 if [[ "$http_code" =~ ^2 ]]; then 184 echo -e "${GREEN} Cache purged successfully${NC}" 185 return 0 186 else 187 echo -e "${RED} Failed to purge cache: HTTP ${http_code}${NC}" 188 return 1 189 fi 190} 191 192# ============================================ 193# MAIN EXECUTION 194# ============================================ 195 196# Step 0: Build the app 197if [ "$SKIP_BUILD" = false ]; then 198 echo "Building app..." 199 npm run build 200 echo "" 201fi 202 203# Check dist directory exists 204if [ ! -d "$DIST_DIR" ]; then 205 echo -e "${RED}Error: ${DIST_DIR} directory not found. Run npm run build first.${NC}" 206 exit 1 207fi 208 209# Build list of local files 210LOCAL_FILES_LIST=$(mktemp) 211REMOTE_PATHS_LIST=$(mktemp) 212trap "rm -f $LOCAL_FILES_LIST $REMOTE_PATHS_LIST" EXIT 213 214# Find all files in dist 215find "$DIST_DIR" -type f | while read -r file; do 216 # Get path relative to dist/ 217 remote_path="${file#${DIST_DIR}/}" 218 echo "$file|$remote_path" >> "$LOCAL_FILES_LIST" 219 echo "$remote_path" >> "$REMOTE_PATHS_LIST" 220done 221 222# Step 1: Upload all local files 223echo "Uploading files..." 224while IFS='|' read -r local_path remote_path; do 225 upload_file "$local_path" "$remote_path" 226done < "$LOCAL_FILES_LIST" 227 228echo "" 229 230# Step 2: Delete orphaned remote files 231echo "Checking for orphaned files..." 232REMOTE_FILES=$(list_remote_files) 233 234if [ -n "$REMOTE_FILES" ]; then 235 while IFS= read -r remote_file; do 236 if [ -z "$remote_file" ]; then 237 continue 238 fi 239 if ! grep -qxF "$remote_file" "$REMOTE_PATHS_LIST"; then 240 delete_file "$remote_file" 241 fi 242 done <<< "$REMOTE_FILES" 243fi 244 245echo "" 246 247# Step 3: Purge CDN cache 248purge_cache 249 250# Summary 251echo "" 252echo "========================================" 253echo -e "${GREEN}Deploy complete!${NC}" 254echo " Uploaded: ${UPLOADED} files" 255echo " Deleted: ${DELETED} files" 256if [ "$DRY_RUN" = true ]; then 257 echo -e "${YELLOW} (DRY RUN - no actual changes made)${NC}" 258fi 259echo "========================================"