captaincore/app/capture/generate
2026-05-30 08:50:06 -04:00

336 lines
10 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# Captures website's pages visually over time based on quicksaves and html changes.
#
# `captaincore capture`
#
# [<site>...]
# One or more sites to check.
#
# [@<target>]
# Target groups of sites like @all @production or @staging.
#
# [--pages=<page>,<page>,<page>...]
# Overrides pages to check. Defaults to site's `capture_pages` configuration.
#
if [ ${#@} -ne 1 ]; then
echo -e "${COLOR_RED}Error:${COLOR_NORMAL} Please specify a <site>."
exit
fi
while read config; do
if [[ "$config" == "Error:"* ]]; then
continue
fi
declare "$config"
done <<< "$(captaincore config fetch --captain-id=$CAPTAIN_ID)"
# Check for API keys
if [[ -z "$screenshots_cloud_api_key" || -z "$screenshots_cloud_api_secret" ]]; then
echo -e "${COLOR_RED}Error:${COLOR_NORMAL} Screenshots Cloud API keys not configured."
exit 1
fi
site=$1
user_agent="captaincore/1.0 (CaptainCore Capture by CaptainCore.io)"
# Use 'magick' (IMv7) if available, fall back to 'convert' (IMv6)
if command -v magick &>/dev/null; then
im_convert="magick"
else
im_convert="convert"
fi
urlencode() {
local _length="${#1}"
for (( _offset = 0 ; _offset < _length ; _offset++ )); do
_print_offset="${1:_offset:1}"
case "${_print_offset}" in
[a-zA-Z0-9.~_-]) printf "${_print_offset}" ;;
' ') printf + ;;
*) printf '%%%X' "'${_print_offset}" ;;
esac
done
}
run_command() {
# Extract environment
if [[ "$site" == *"-staging"* ]]; then
environment=staging
else
environment=production
fi
# Load site configs
IFS=$'\n'$'\r'; for line in $(captaincore site get $site --bash --captain-id=$CAPTAIN_ID); do declare "$line"; done; unset IFS
# Return error if domain not found
if [[ "$domain" == "" ]] || [[ "$site" == "" ]]; then
echo "Can't locate website for $site"
continue
fi
if [[ "$home_url" == "" ]]; then
echo -e "${COLOR_RED}Error:${COLOR_NORMAL} Site $domain is missing home_url. Recommend running sync-data first."
continue
fi
if [[ "$pages" != "" ]]; then
echo "Overidding default page selection."
capture_pages=$pages
fi
if [[ "$capture_pages" == "" ]]; then
capture_pages="/"
fi
# Append plugin-discovered pages (checkout/cart/account on active ecommerce plugins).
# These are tracked for HTML/JS drift but skip the Screenshots Cloud screenshot.
declare -A nosnap_pages
if [[ "$capture_plugin_pages" != "" ]]; then
IFS=','; for p in $capture_plugin_pages; do
[[ -z "$p" ]] && continue
# De-dupe: skip if already in user-configured capture_pages
if [[ ",${capture_pages}," != *",${p},"* ]]; then
capture_pages="${capture_pages},${p}"
fi
nosnap_pages["$p"]=1
done; unset IFS
fi
echo "Capturing site ${site}-${environment} for pages $capture_pages"
# Define remote paths
site_path=${site}_${site_id}/${environment}
remote_path_captures=${site}_${site_id}/${environment}/captures
remote_path_screenshots=${site}_${site_id}/${environment}/screenshots
if [[ "$captaincore_fleet" == "true" ]]; then
remote_path_captures=${CAPTAIN_ID}/${remote_path_captures}
remote_path_screenshots=${CAPTAIN_ID}/${remote_path_screenshots}
fi
# Append trailing slash if home_directory exist
if [ "$home_directory" != "" ]; then
home_directory="${home_directory}/"
fi
# Current timestamp
timedate=$(date +%Y-%m-%d_%H-%M-%S)
# Prepare git repo
mkdir -p $path/$site_path/captures/
cd $path/$site_path/captures/
# Create new git repo if needed
if [ ! -d ".git" ]; then
git init
echo "/.screenshots" > .gitignore
fi
# Remove current captures if needed
current_capture_count=$( ls *.capture | wc -l )
if [[ "$current_capture_count" != "0" ]]; then
rm *.capture
fi
# Create array of pages to capture
IFS=',';pages=($capture_pages);unset IFS;
captured_pages=""
# Loop through and capture
for i in `seq 1 ${#pages[@]}`; do
selected=$( expr $i - 1 )
page=${pages[$selected]}
file_name="${page//\//#}.capture"
echo "Capturing ${home_url}${page} to ${page//\//#}.capture"
curl -L --max-time 30 --compressed --silent "${home_url}${page}" > "$file_name"
# Continue if no changes found
check_status=$( git status "$file_name" -s )
if [[ "$check_status" == "" ]]; then
continue
fi
git add "$file_name"
check_status_again=$( git status "$file_name" -s )
if [[ "$check_status_again" == "A"* || "$check_status_again" == "M"* || "$check_status_again" == "R"* ]]; then
captured_pages="${captured_pages}${page},"
# Plugin-discovered pages (checkout/cart/account): skip screenshot, just track HTML diff.
# Drift will still flow through `capture check` and trigger alerts on new scripts.
if [[ "${nosnap_pages[$page]}" == "1" ]]; then
echo "Skipping screenshot for auto-discovered page ${page}"
continue
fi
mkdir -p ".screenshots/"
target_file=".screenshots/${page//\//#}_working.jpg"
max_retries=5
attempt=1
while [ $attempt -le $max_retries ]; do
query="url=$( urlencode "${home_url}${page}" )&full_page=true&viewport_width=1280&force=true&user_agent=$( urlencode "$user_agent" )&format=jpg&pixel_ratio=2"
if [[ "$auth" != "" ]]; then
query="url=$( urlencode "${home_url}${page}" )&authorization=$( urlencode "Basic $auth" )&full_page=true&viewport_width=1280&force=true&user_agent=$( urlencode "$user_agent" )&format=jpg&pixel_ratio=2"
fi
screenshots_cloud_token=$( printf '%s' "$query" | openssl sha1 -hmac "$screenshots_cloud_api_secret" | sed 's/^.* //' )
echo "Downloading screenshot for ${home_url}${page} (Attempt $attempt of $max_retries)"
http_code=$(curl --silent --output "$target_file" --write-out "%{http_code}" --max-time 60 "https://api.screenshots.cloud/v1/screenshot/$screenshots_cloud_api_key/$screenshots_cloud_token/?$query")
# Validate the downloaded file
file_size=0
mime_type="unknown"
if [ -f "$target_file" ]; then
file_size=$(wc -c < "$target_file" | tr -d ' ')
mime_type=$(file --mime-type -b "$target_file")
fi
if [ "$http_code" = "200" ] && [ "$file_size" -gt 1000 ] && [[ "$mime_type" == "image/"* ]]; then
echo "Screenshot captured successfully (${file_size} bytes)"
break
fi
echo "Attempt $attempt failed (HTTP: $http_code, size: ${file_size} bytes, type: $mime_type)"
if [ $attempt -lt $max_retries ]; then
echo "Retrying in 10 seconds..."
sleep 10
else
echo "Failed to capture valid screenshot after $max_retries attempts. Skipping."
rm -f "$target_file"
fi
attempt=$((attempt + 1))
done
fi
done
# Add all files to git repo
git add -A
# Current git status
git_status=$(git status -s)
if [[ "$git_status" == "" && "$force" != "true" ]]; then
# Skip capture as nothing changed
echo "No changes found."
exit
fi
# New commit
git commit -m "Captured at $timedate"
# Save git hash
git_commit=$(git log -n 1 --pretty=format:"%H") # Get hash of last commit (commit hash)
git_commit_short=$(git log -n 1 --pretty=format:"%h")
git_status=$(git show $git_commit --shortstat --format=)
created_at=$(git show -s --pretty=format:"%ct" $git_commit) # Get date of last commit (UNIX timestamp)
files_changed=$( git show $git_commit --name-status --format= )
# Organize new screenshots. Rename all files working_* to ${date}_${git_commit_short}_
for image in $( ls .screenshots/*_working.jpg 2>/dev/null ); do
new_ending="_${created_at}_${git_commit_short}"
new_image_name=${image/_working/$new_ending}
# Ensure we only process if the file has content and is a valid image
if [ -s "$image" ] && identify "$image" >/dev/null 2>&1; then
# Generate home page thumbnails
if [[ "$image" == ".screenshots/#_working.jpg" ]]; then
$im_convert ".screenshots/#_working.jpg" -thumbnail 800 -gravity North -crop 800x500+0+0 ".screenshots/${created_at}_${git_commit_short}_thumb-800.jpg"
$im_convert ".screenshots/${created_at}_${git_commit_short}_thumb-800.jpg" -thumbnail 100 ".screenshots/${created_at}_${git_commit_short}_thumb-100.jpg"
# Check if files exist before moving to prevent rclone errors
if [ -f ".screenshots/${created_at}_${git_commit_short}_thumb-100.jpg" ]; then
rclone move ".screenshots/${created_at}_${git_commit_short}_thumb-100.jpg" ${rclone_upload}${remote_path_screenshots}/ --fast-list --transfers=32 --no-update-modtime --progress --stats-one-line --stats=1m
fi
if [ -f ".screenshots/${created_at}_${git_commit_short}_thumb-800.jpg" ]; then
rclone move ".screenshots/${created_at}_${git_commit_short}_thumb-800.jpg" ${rclone_upload}${remote_path_screenshots}/ --fast-list --transfers=32 --no-update-modtime --progress --stats-one-line --stats=1m
fi
fi
mv "$image" "$new_image_name"
else
echo "Skipping/Removing invalid image: $image"
rm -f "$image"
fi
done
# Copy to remote storage if directory is not empty
if [ -d ".screenshots/" ] && [ "$(ls -A .screenshots/)" ]; then
rclone move .screenshots/ ${rclone_upload}${remote_path_captures}/ --fast-list --transfers=32 --no-update-modtime --progress --stats-one-line --stats=1m
fi
# Build json for capture
read -r -d '' capture << EOM
{
"git_commit":"$git_commit",
"capture_pages":"$capture_pages",
"captured_pages":"${captured_pages%?}",
"created_at":"$created_at"
}
EOM
echo "Git hash $git_commit"
if [[ "$debug" == "true" ]]; then
read -r -d '' VAR << EOM
{
"command": "new-capture",
"site_id":"$site_id",
"environment":"$environment",
"data": $capture,
"token":"$token"
}
EOM
echo $VAR
continue
fi
# Adds capture to CaptainCore
if [[ "$captaincore_dev" == true ]]; then
curl_argument="-k"
fi
# Adds capture to CaptainCore
curl --silent --output /dev/null ${curl_argument} --request POST "$captaincore_api" --header "Content-Type: application/json" --data @- << EOM
{
"command": "new-capture",
"site_id":"$site_id",
"environment":"$environment",
"data": $capture,
"token":"$token"
}
EOM
# Check for injected scripts/stylesheets
captaincore capture check ${site_id}-${environment} --captain-id=$CAPTAIN_ID
# Generate capture usage stats
capture_count=$( git rev-list --all --count )
# Sync site data, pull in new thumbnails
captaincore sync-data $site_id --captain-id=$CAPTAIN_ID
# Clear out variables
site=''
domain=''
home_directory=''
subsite=''
response=''
response_parsed=''
# End logging
echo "$(date +'%Y-%m-%d %H:%M') Finished capture"
}
run_command