|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +# --- Configuration (Non-Overridable) --- |
| 4 | +APP_NAME="dummy" |
| 5 | +APP_DIR=".dummy-app" |
| 6 | +DOWNLOAD_URL="https://example.com/releases/.../foo-latest.tgz" |
| 7 | +# --------------------------------------- |
| 8 | + |
| 9 | +# 1. Setup essential variables needed for config loading |
| 10 | +HOME_DIR="$HOME" |
| 11 | +APP_HOME="$HOME_DIR/$APP_DIR" |
| 12 | + |
| 13 | +# 2. Define default overridable variables |
| 14 | +# The update period in days (e.g., 3 means check if the last_checked file is older than 3 days) |
| 15 | +UPDATE_PERIOD=3 |
| 16 | +# Logging level (default empty/silent; set to anything, e.g., 'DEBUG', to enable logging) |
| 17 | +LOG_LEVEL= |
| 18 | + |
| 19 | +# --- Configuration Loading --- |
| 20 | +# Function to load configuration variables from a file |
| 21 | +load_config() { |
| 22 | + local config_file="$1" |
| 23 | + if [ -f "$config_file" ]; then |
| 24 | + #LOG "Loading configuration from $config_file..." |
| 25 | + source "$config_file" 2>/dev/null |
| 26 | + fi |
| 27 | +} |
| 28 | + |
| 29 | +# Load user-wide configuration (if exists) |
| 30 | +load_config "$APP_HOME/bootstrap.cfg" |
| 31 | + |
| 32 | +# Load local configuration (if exists) |
| 33 | +load_config "./$APP_DIR/bootstrap.cfg" |
| 34 | + |
| 35 | +# ----------------------------- |
| 36 | + |
| 37 | +# 3. Define the remaining path variables using the (potentially overridden) APP_NAME/UPDATE_PERIOD |
| 38 | +CACHE_DIR="$APP_HOME/_cache" |
| 39 | +BIN_DIR="$APP_HOME/bin" |
| 40 | +APP_EXE="$BIN_DIR/$APP_NAME" |
| 41 | +ARCHIVE_FILE="$CACHE_DIR/release.tgz" |
| 42 | +LAST_CHECKED_FILE="$CACHE_DIR/last_checked" |
| 43 | + |
| 44 | +LOG() { |
| 45 | + # Only echo the log message if LOG_LEVEL is not empty |
| 46 | + if [ -n "$LOG_LEVEL" ]; then |
| 47 | + echo "[$APP_NAME Bootstrap] $1" |
| 48 | + fi |
| 49 | +} |
| 50 | + |
| 51 | +# Function to get the last modification timestamp of a file |
| 52 | +get_file_mtime() { |
| 53 | + # Use stat command for reliable cross-platform (Linux/Mac) date retrieval |
| 54 | + # Linux: 'stat -c %Y' for seconds since epoch |
| 55 | + # macOS: 'stat -f %m' for seconds since epoch |
| 56 | + if command -v gstat &> /dev/null; then |
| 57 | + # Use GNU stat if available (e.g., via MacPorts/Homebrew) |
| 58 | + gstat -c %Y "$1" 2>/dev/null |
| 59 | + elif uname | grep -q "Darwin"; then |
| 60 | + # macOS |
| 61 | + stat -f %m "$1" 2>/dev/null |
| 62 | + else |
| 63 | + # Standard Linux |
| 64 | + stat -c %Y "$1" 2>/dev/null |
| 65 | + fi |
| 66 | +} |
| 67 | + |
| 68 | +# --- Core Logic Functions --- |
| 69 | + |
| 70 | +perform_full_install() { |
| 71 | + LOG "Application not found or update forced. Starting download and install..." |
| 72 | + |
| 73 | + # 1. Setup directories |
| 74 | + mkdir -p "$CACHE_DIR" || { LOG "Error: Could not create cache directory $CACHE_DIR"; exit 1; } |
| 75 | + |
| 76 | + # 2. Download the release archive |
| 77 | + LOG "Downloading $DOWNLOAD_URL..." |
| 78 | + # The --remote-time (-r) option ensures the local file date matches the remote server's date. |
| 79 | + if ! curl -fsSL --remote-time "$DOWNLOAD_URL" -o "$ARCHIVE_FILE"; then |
| 80 | + LOG "Error: Download failed for $DOWNLOAD_URL." |
| 81 | + exit 1 |
| 82 | + fi |
| 83 | + |
| 84 | + # 3. Unpack and clean up |
| 85 | + LOG "Unpacking application..." |
| 86 | + # Remove existing installation directory if it exists before unpacking |
| 87 | + rm -rf "$APP_HOME/temp_install" |
| 88 | + mkdir -p "$APP_HOME/temp_install" |
| 89 | + |
| 90 | + if ! tar -xzf "$ARCHIVE_FILE" -C "$APP_HOME/temp_install"; then |
| 91 | + LOG "Error: Failed to unpack archive $ARCHIVE_FILE." |
| 92 | + rm -rf "$APP_HOME/temp_install" |
| 93 | + exit 1 |
| 94 | + fi |
| 95 | + |
| 96 | + # Find the top-level directory inside the archive (often matches the APP_NAME) |
| 97 | + # Move the contents of the single directory inside the archive to the app home |
| 98 | + # Assuming the archive unpacks to a single directory (e.g., 'foo-v1.0') |
| 99 | + UNPACKED_ROOT=$(find "$APP_HOME/temp_install" -mindepth 1 -maxdepth 1 -type d -print -quit) |
| 100 | + |
| 101 | + if [ -d "$UNPACKED_ROOT" ]; then |
| 102 | + # Move contents of the single directory to APP_HOME |
| 103 | + LOG "Moving contents from $UNPACKED_ROOT to $APP_HOME" |
| 104 | + # Move all contents, excluding the original temp directory itself |
| 105 | + mv "$UNPACKED_ROOT"/* "$APP_HOME" 2>/dev/null |
| 106 | + mv "$UNPACKED_ROOT"/.* "$APP_HOME" 2>/dev/null # Move hidden files/dirs too |
| 107 | + else |
| 108 | + # If no single directory, move all contents of temp_install to APP_HOME |
| 109 | + LOG "Moving contents directly from $APP_HOME/temp_install to $APP_HOME" |
| 110 | + mv "$APP_HOME/temp_install"/* "$APP_HOME" 2>/dev/null |
| 111 | + mv "$APP_HOME/temp_install"/.* "$APP_HOME" 2>/dev/null # Move hidden files/dirs too |
| 112 | + fi |
| 113 | + |
| 114 | + # Clean up the temporary directory |
| 115 | + rm -rf "$APP_HOME/temp_install" |
| 116 | + |
| 117 | + # 4. Create/update the last_checked file |
| 118 | + touch "$LAST_CHECKED_FILE" |
| 119 | + LOG "Installation complete in $APP_HOME." |
| 120 | +} |
| 121 | + |
| 122 | + |
| 123 | +handle_update_check() { |
| 124 | + # 1. Check file age: older than $UPDATE_PERIOD days? |
| 125 | + # find command exits 0 and prints if a file matching criteria is found. |
| 126 | + # -mtime +N means modification time is older than N full 24-hour periods. |
| 127 | + if [ ! -f "$LAST_CHECKED_FILE" ] || find "$LAST_CHECKED_FILE" -mtime +$UPDATE_PERIOD -print -quit 2>/dev/null; then |
| 128 | + LOG "Checking for updates (last check older than $UPDATE_PERIOD days or file missing)..." |
| 129 | + |
| 130 | + # Record the modification time of the current archive before potential download |
| 131 | + local OLD_MTIME=$(get_file_mtime "$ARCHIVE_FILE") |
| 132 | + |
| 133 | + # 2. Conditional Download |
| 134 | + # -z "$ARCHIVE_FILE" tells curl to only download if the remote file is newer than the local one. |
| 135 | + # --remote-time (-r) ensures the new file uses the remote timestamp. |
| 136 | + LOG "Attempting conditional download..." |
| 137 | + |
| 138 | + # Use -w "%{http_code}" to capture the status code |
| 139 | + HTTP_CODE=$(curl -w "%{http_code}" -sSL --remote-time -z "$ARCHIVE_FILE" "$DOWNLOAD_URL" -o "$ARCHIVE_FILE") |
| 140 | + |
| 141 | + # 3. Check for successful download (HTTP 200) or successful conditional skip (HTTP 304 Not Modified) |
| 142 | + if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then |
| 143 | + # Download successful (200 OK) |
| 144 | + LOG "New release downloaded." |
| 145 | + |
| 146 | + # Record the new modification time of the archive |
| 147 | + local NEW_MTIME=$(get_file_mtime "$ARCHIVE_FILE") |
| 148 | + |
| 149 | + # Check if the file timestamp actually changed (i.e., a newer file was downloaded) |
| 150 | + if [ "$NEW_MTIME" -ne "$OLD_MTIME" ]; then |
| 151 | + LOG "Archive file date changed (old=$OLD_MTIME, new=$NEW_MTIME). Unpacking update." |
| 152 | + # 4. Unpack the new release |
| 153 | + perform_full_install |
| 154 | + else |
| 155 | + LOG "Remote file was newer, but timestamp remained the same or file was skipped by curl -z. Skipping unpack." |
| 156 | + fi |
| 157 | + elif [ "$HTTP_CODE" -eq 304 ]; then |
| 158 | + # Conditional download skipped (304 Not Modified) |
| 159 | + LOG "No new release available (304 Not Modified)." |
| 160 | + else |
| 161 | + # Other error or redirection |
| 162 | + LOG "Conditional download failed or returned unexpected status code: $HTTP_CODE. Skipping update." |
| 163 | + fi |
| 164 | + |
| 165 | + # 5. Update last_checked timestamp regardless of success/failure of the update attempt |
| 166 | + touch "$LAST_CHECKED_FILE" |
| 167 | + LOG "Update check complete. Timestamp updated." |
| 168 | + else |
| 169 | + LOG "Skipping update check (last check within $UPDATE_PERIOD days)." |
| 170 | + fi |
| 171 | +} |
| 172 | + |
| 173 | +# --- Execution Flow --- |
| 174 | + |
| 175 | +# 1. Installation/Update Check |
| 176 | +if [ ! -f "$APP_EXE" ]; then |
| 177 | + perform_full_install |
| 178 | +else |
| 179 | + # Ensure cache directory exists for the last_checked file |
| 180 | + mkdir -p "$CACHE_DIR" 2>/dev/null |
| 181 | + handle_update_check |
| 182 | +fi |
| 183 | + |
| 184 | +# 2. Execution Handover (The final requirement) |
| 185 | +# Check if the currently executing script ($0) is NOT the application itself |
| 186 | +# Use realpath to resolve symbolic links and get the canonical path |
| 187 | +CURRENT_SCRIPT_PATH=$(realpath "$0") |
| 188 | +BIN_EXE_PATH=$(realpath "$APP_EXE") |
| 189 | + |
| 190 | +# Check if the current script path is NOT the path of the actual application executable |
| 191 | +if [ "$CURRENT_SCRIPT_PATH" != "$BIN_EXE_PATH" ]; then |
| 192 | + LOG "Handing over execution to the installed application: $APP_EXE" |
| 193 | + # exec replaces the current script process with the application process |
| 194 | + exec "$APP_EXE" "$@" |
| 195 | +fi |
| 196 | + |
| 197 | +########################################## |
| 198 | +# BELOW THIS POINT YOU PUT YOUR OWN CODE # |
| 199 | +########################################## |
| 200 | + |
| 201 | +# This part is the "whatever might be there" section. |
| 202 | +LOG "Running inside the application's environment ($BIN_DIR)." |
| 203 | + |
| 204 | +# Your application's main logic or final execution step would go here if this script |
| 205 | +# *is* the final executable. Otherwise you can call out to other scripts or binaries as needed. |
| 206 | + |
| 207 | +# For simplicity, we just print a success message and exit. |
| 208 | +echo "------------------------------------" |
| 209 | +echo "This is application: $APP_NAME" |
| 210 | +echo "Arguments received : $@" |
| 211 | +echo "------------------------------------" |
| 212 | + |
| 213 | +exit 0 |
0 commit comments