WIP PWA for Grain
next.grain.social
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 "========================================"