forked from
slices.network/quickslice
Auto-indexing service and GraphQL API for AT Protocol Records
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 "============================================"