Auto-indexing service and GraphQL API for AT Protocol Records
at main 272 lines 7.3 kB view raw
1#!/usr/bin/env bash 2set -euo pipefail 3 4# Bunny CDN Deploy Script 5# Syncs www/priv/ to Bunny Storage Zone with full sync and cache purge 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 16SKIPPED=0 17 18# Parse arguments 19DRY_RUN=false 20VERBOSE=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 *) 33 echo -e "${RED}Unknown option: $1${NC}" 34 exit 1 35 ;; 36 esac 37done 38 39# Load .env file if it exists 40if [ -f .env ]; then 41 set -a 42 source .env 43 set +a 44fi 45 46# Required environment variables 47: "${BUNNY_API_KEY:?BUNNY_API_KEY environment variable is required (Account API key for cache purge)}" 48: "${BUNNY_STORAGE_PASSWORD:?BUNNY_STORAGE_PASSWORD environment variable is required (Storage Zone password)}" 49: "${BUNNY_STORAGE_ZONE:?BUNNY_STORAGE_ZONE environment variable is required}" 50: "${BUNNY_STORAGE_HOST:?BUNNY_STORAGE_HOST environment variable is required (e.g., storage.bunnycdn.com)}" 51: "${BUNNY_PULLZONE_ID:?BUNNY_PULLZONE_ID environment variable is required}" 52 53# Configuration 54LOCAL_DIR="www/priv" 55STORAGE_URL="https://${BUNNY_STORAGE_HOST}/${BUNNY_STORAGE_ZONE}" 56 57echo "Bunny CDN Deploy" 58echo "================" 59echo "Storage Zone: ${BUNNY_STORAGE_ZONE}" 60echo "Storage Host: ${BUNNY_STORAGE_HOST}" 61echo "Local Dir: ${LOCAL_DIR}" 62if [ "$DRY_RUN" = true ]; then 63 echo -e "${YELLOW}DRY RUN MODE - No changes will be made${NC}" 64fi 65echo "" 66 67# Get content-type based on file extension 68get_content_type() { 69 local file="$1" 70 case "${file##*.}" in 71 html) echo "text/html" ;; 72 css) echo "text/css" ;; 73 js) echo "application/javascript" ;; 74 json) echo "application/json" ;; 75 png) echo "image/png" ;; 76 jpg|jpeg) echo "image/jpeg" ;; 77 gif) echo "image/gif" ;; 78 svg) echo "image/svg+xml" ;; 79 ico) echo "image/x-icon" ;; 80 woff) echo "font/woff" ;; 81 woff2) echo "font/woff2" ;; 82 *) echo "application/octet-stream" ;; 83 esac 84} 85 86# Upload a single file 87upload_file() { 88 local local_path="$1" 89 local remote_path="$2" 90 local content_type 91 content_type=$(get_content_type "$local_path") 92 93 if [ "$VERBOSE" = true ]; then 94 echo " Uploading: ${remote_path} (${content_type})" 95 fi 96 97 if [ "$DRY_RUN" = true ]; then 98 ((UPLOADED++)) 99 return 0 100 fi 101 102 local response 103 local http_code 104 105 response=$(curl -s -w "\n%{http_code}" -X PUT \ 106 "${STORAGE_URL}/${remote_path}" \ 107 -H "AccessKey: ${BUNNY_STORAGE_PASSWORD}" \ 108 -H "Content-Type: ${content_type}" \ 109 --data-binary "@${local_path}") 110 111 http_code=$(echo "$response" | tail -n1) 112 113 if [[ "$http_code" =~ ^2 ]]; then 114 ((UPLOADED++)) 115 return 0 116 else 117 echo -e "${RED}Failed to upload ${remote_path}: HTTP ${http_code}${NC}" 118 echo "$response" | head -n -1 119 return 1 120 fi 121} 122 123# List all files in remote storage (recursively) 124list_remote_files() { 125 local path="${1:-}" 126 local url="${STORAGE_URL}/${path}" 127 128 local response 129 response=$(curl -s -X GET "$url" \ 130 -H "AccessKey: ${BUNNY_STORAGE_PASSWORD}" \ 131 -H "Accept: application/json") 132 133 # Parse JSON response - each item has ObjectName and IsDirectory 134 echo "$response" | jq -r '.[] | 135 if .IsDirectory then 136 .ObjectName + "/" 137 else 138 .ObjectName 139 end' 2>/dev/null | while read -r item; do 140 if [[ "$item" == */ ]]; then 141 # It's a directory, recurse 142 local subdir="${item%/}" 143 if [ -n "$path" ]; then 144 list_remote_files "${path}${subdir}/" 145 else 146 list_remote_files "${subdir}/" 147 fi 148 else 149 # It's a file, print full path 150 echo "${path}${item}" 151 fi 152 done 153} 154 155# Delete a single file from remote 156delete_file() { 157 local remote_path="$1" 158 159 if [ "$VERBOSE" = true ]; then 160 echo " Deleting: ${remote_path}" 161 fi 162 163 if [ "$DRY_RUN" = true ]; then 164 ((DELETED++)) 165 return 0 166 fi 167 168 local http_code 169 http_code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ 170 "${STORAGE_URL}/${remote_path}" \ 171 -H "AccessKey: ${BUNNY_STORAGE_PASSWORD}") 172 173 if [[ "$http_code" =~ ^2 ]]; then 174 ((DELETED++)) 175 return 0 176 else 177 echo -e "${RED}Failed to delete ${remote_path}: HTTP ${http_code}${NC}" 178 return 1 179 fi 180} 181 182# Purge pull zone cache 183purge_cache() { 184 echo "Purging CDN cache..." 185 186 if [ "$DRY_RUN" = true ]; then 187 echo -e "${YELLOW} Would purge pull zone ${BUNNY_PULLZONE_ID}${NC}" 188 return 0 189 fi 190 191 local http_code 192 http_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ 193 "https://api.bunny.net/pullzone/${BUNNY_PULLZONE_ID}/purgeCache" \ 194 -H "AccessKey: ${BUNNY_API_KEY}" \ 195 -H "Content-Type: application/json") 196 197 if [[ "$http_code" =~ ^2 ]]; then 198 echo -e "${GREEN} Cache purged successfully${NC}" 199 return 0 200 else 201 echo -e "${RED} Failed to purge cache: HTTP ${http_code}${NC}" 202 return 1 203 fi 204} 205 206# ============================================ 207# MAIN EXECUTION 208# ============================================ 209 210# Check local directory exists 211if [ ! -d "$LOCAL_DIR" ]; then 212 echo -e "${RED}Error: Local directory ${LOCAL_DIR} does not exist${NC}" 213 echo "Run 'make docs' first to generate documentation" 214 exit 1 215fi 216 217# Fingerprint styles.css for cache busting 218echo "Fingerprinting styles.css..." 219if [ -f "${LOCAL_DIR}/styles.css" ]; then 220 HASH=$(md5 -q "${LOCAL_DIR}/styles.css" | cut -c1-8) 221 mv "${LOCAL_DIR}/styles.css" "${LOCAL_DIR}/styles.${HASH}.css" 222 find "$LOCAL_DIR" -name "*.html" -exec sed -i '' "s|/styles.css|/styles.${HASH}.css|g" {} \; 223 echo -e "${GREEN} styles.css -> styles.${HASH}.css${NC}" 224else 225 echo -e "${YELLOW} No styles.css found, skipping${NC}" 226fi 227echo "" 228 229# Step 1: Upload all local files 230echo "Uploading files..." 231LOCAL_FILES_LIST=$(mktemp) 232trap "rm -f $LOCAL_FILES_LIST" EXIT 233 234find "$LOCAL_DIR" -type f -print0 | while IFS= read -r -d '' file; do 235 # Get path relative to LOCAL_DIR 236 relative_path="${file#${LOCAL_DIR}/}" 237 echo "$relative_path" >> "$LOCAL_FILES_LIST" 238 upload_file "$file" "$relative_path" 239done 240 241echo "" 242 243# Step 2: Delete orphaned remote files (full sync) 244echo "Checking for orphaned files..." 245REMOTE_FILES=$(list_remote_files) 246 247if [ -n "$REMOTE_FILES" ]; then 248 while IFS= read -r remote_file; do 249 if [ -z "$remote_file" ]; then 250 continue 251 fi 252 if ! grep -qxF "$remote_file" "$LOCAL_FILES_LIST"; then 253 delete_file "$remote_file" 254 fi 255 done <<< "$REMOTE_FILES" 256fi 257 258echo "" 259 260# Step 3: Purge CDN cache 261purge_cache 262 263# Summary 264echo "" 265echo "============================================" 266echo -e "${GREEN}Deploy complete!${NC}" 267echo " Uploaded: ${UPLOADED} files" 268echo " Deleted: ${DELETED} files" 269if [ "$DRY_RUN" = true ]; then 270 echo -e "${YELLOW} (DRY RUN - no actual changes made)${NC}" 271fi 272echo "============================================"