From e64cbf5334556f4a9e01050eabb9bd7af8ecd8cb Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 27 Aug 2026 08:58:47 -0400 Subject: [PATCH 1/5] Enhance updatePanel.sh --- static/updatePanel.sh | 685 ++++++++++++++++++++++++++++++++---------- 1 file changed, 534 insertions(+), 151 deletions(-) diff --git a/static/updatePanel.sh b/static/updatePanel.sh index 198ff4f..5b87f17 100644 --- a/static/updatePanel.sh +++ b/static/updatePanel.sh @@ -1,12 +1,102 @@ #!/bin/bash +# Pelican Panel – Incremental Git-Based Update Script +# Applies only the files that changed between the current version and the latest release. +set -euo pipefail + +# ───────────────────────────────────────────────────────────────────────────── +# 0a. Dependency check – git must be available +# ───────────────────────────────────────────────────────────────────────────── +if ! command -v git &>/dev/null; then + echo "git is not installed or not in PATH. Please install git and re-run this script." >&2 + exit 1 +fi + +# ───────────────────────────────────────────────────────────────────────────── +# 0b. Root check +# ───────────────────────────────────────────────────────────────────────────── if [[ $EUID -ne 0 ]]; then - echo "This script must be run as root or with sudo." >&2 - exit 1 + echo "This script must be run as root or with sudo." >&2 + exit 1 fi -read -p "Enter the directory for the panel location [/var/www/pelican]: " install_dir -install_dir=${install_dir:-/var/www/pelican} +PANEL_REPO="https://github.com/pelican/panel.git" +TIMESTAMP=$(date +"%Y%m%d_%H%M%S") + +# ───────────────────────────────────────────────────────────────────────────── +# 0c. Log file – tee all output so we can upload it later +# ───────────────────────────────────────────────────────────────────────────── +LOG_FILE="/tmp/pelican_update_${TIMESTAMP}.log" +exec > >(tee -a "$LOG_FILE") 2>&1 +echo "Update log: $LOG_FILE" + +# Upload log to logs.pelican.dev and print the URL. +upload_log() { + if ! command -v curl &>/dev/null; then + echo "curl not found – cannot upload log." >&2 + return 1 + fi + local content + content=$(cat "$LOG_FILE") + local response + response=$(curl -s -w "\n%{http_code}" \ + -F "c=$content" \ + -F "e=14d" \ + "https://logs.pelican.dev") + local http_code + http_code=$(echo "$response" | tail -n1) + local body + body=$(echo "$response" | sed '$d') + if [ "$http_code" = "200" ]; then + # Parse the url field from JSON response + local paste_url + paste_url=$(echo "$body" | sed -n 's/.*"url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' || true) + if [ -n "$paste_url" ]; then + echo "" + echo " ✓ Log uploaded." + echo " URL: $paste_url" + echo "" + else + echo "Uploaded but could not parse URL from response: $body" + fi + else + echo "Upload failed (HTTP $http_code): $body" >&2 + return 1 + fi +} + +# Offer to upload the log – called on success and on error traps. +offer_log_upload() { + echo "" + echo "Log saved to: $LOG_FILE" + echo "Note: the log contains command output, file paths, and version information from this run." + read -rp "Upload log to logs.pelican.dev to share? (y/n) [n]: " upload_confirm /dev/null || echo "www-data") +read -rp "Enter the owner of the files [$owner]: " owner_input +owner="${owner_input:-$owner}" + +group=$(stat -c '%G' "$install_dir" 2>/dev/null || echo "www-data") +read -rp "Enter the group of the files [$group]: " group_input +group="${group_input:-$group}" + +read -rp "Show detailed file change list during update? (y/n) [n]: " verbose_confirm \s*'\K[^']+" "$config_app" | tr -d '[:space:]' || true) +fi + +if [ -z "$current_version" ]; then + read -rp "Could not detect current version from config/app.php. Enter it manually (e.g. v1.0.0-beta34): " current_version +fi + +# Normalise: ensure leading 'v' +[[ "$current_version" != v* ]] && current_version="v${current_version}" +echo "Current installed version: $current_version" -group=$(stat -c '%G' "$install_dir" || echo "www-data") -read -p "Enter the group of the files (www-data, apache, nginx) [$group]: " group -group=${group:-www-data} +# ───────────────────────────────────────────────────────────────────────────── +# 4. Clone / update a local mirror of the panel repo +# ───────────────────────────────────────────────────────────────────────────── +tmp_repo="/tmp/pelican_panel_repo_${TIMESTAMP}" +echo "" +echo "Cloning Pelican Panel repository to $tmp_repo (this may take a moment)..." +git clone --bare "$PANEL_REPO" "$tmp_repo" --quiet -db_connection=$(grep "^DB_CONNECTION=" "$env_file" | cut -d '=' -f 2 | tr -d "\"'") +# Point git at the bare repo without cd-ing into it. +# Some git versions (safe.bareRepository=explicit) refuse diff/show when the +# bare repo is merely the CWD; setting GIT_DIR explicitly is always accepted. +export GIT_DIR="$tmp_repo" -if [ -z "$db_connection" ]; then - db_connection='sqlite' - echo "DB_CONNECTION not found in $env_file, using $db_connection as default." -else - echo "DB_CONNECTION is set to: $db_connection" -fi +# Fetch all tags +git fetch --tags --quiet -read -p "Do you want to create a backup? (y/n) [y]: " backup_confirm -backup_confirm=${backup_confirm:-y} -if [ "$backup_confirm" != "y" ]; then - echo "Backup canceled." +# ───────────────────────────────────────────────────────────────────────────── +# 5. Collect and sort version tags +# ───────────────────────────────────────────────────────────────────────────── +mapfile -t all_tags < <(git tag -l 'v*' | sort -V) + +if [ ${#all_tags[@]} -eq 0 ]; then + echo "No version tags found in the repository. Exiting..." + rm -rf "$tmp_repo" exit 1 fi -backup_dir="$install_dir/backup" -mkdir -p "$backup_dir/storage/app" -if [ $? -ne 0 ]; then - echo "Failed to create backup directory $backup_dir, aborting" - exit 1 +latest_version="${all_tags[-1]}" +echo "Latest available version: $latest_version" + +if [ "$current_version" = "$latest_version" ]; then + echo "Panel is already up to date ($current_version). Nothing to do." + rm -rf "$tmp_repo" + trap - ERR + offer_log_upload + exit 0 fi -cp -a "$env_file" "$backup_dir/.env.backup" -if [ $? -ne 0 ]; then - echo "Failed to backup .env file, aborting" +# Build the ordered list of versions strictly newer than current +upgrade_path=() +found_current=false +for tag in "${all_tags[@]}"; do + if [ "$tag" = "$current_version" ]; then + found_current=true + continue + fi + if $found_current; then + upgrade_path+=("$tag") + fi +done + +if [ ${#upgrade_path[@]} -eq 0 ]; then + if ! $found_current; then + echo "WARNING: Current version tag '$current_version' not found in repository." + echo "Cannot determine safe upgrade path. Exiting." + else + echo "Panel is already at the latest known tag ($current_version). Nothing to do." + fi + rm -rf "$tmp_repo" exit 1 fi -echo "Backed up .env file successfully." -if [ -d "$install_dir/storage/app/public" ]; then - cp -a "$install_dir/storage/app/public" "$backup_dir/storage/app/" - if [ $? -ne 0 ]; then - echo "Failed to backup avatars & fonts files, aborting" - exit 1 +echo "" +echo "Upgrade path:" +prev="$current_version" +for v in "${upgrade_path[@]}"; do + echo " $prev -> $v" + prev="$v" +done + +# ───────────────────────────────────────────────────────────────────────────── +# 6. DB check +# ───────────────────────────────────────────────────────────────────────────── +db_connection=$(grep "^DB_CONNECTION=" "$env_file" | cut -d '=' -f2 | tr -d "\"'" || echo "sqlite") +db_connection="${db_connection:-sqlite}" +echo "" +echo "DB_CONNECTION: $db_connection" + +db_database="" +if [ "$db_connection" = "sqlite" ]; then + db_database=$(grep "^DB_DATABASE=" "$env_file" | cut -d '=' -f2 | tr -d "\"'" || true) + db_database="${db_database:-database/database.sqlite}" + # Resolve relative paths against the install directory + if [[ "$db_database" != /* ]]; then + db_database="$install_dir/$db_database" fi + # If the resolved path doesn't exist but the file lives in the database/ subdirectory, try that + if [ ! -f "$db_database" ] && [ -f "$install_dir/database/$(basename "$db_database")" ]; then + db_database="$install_dir/database/$(basename "$db_database")" + fi + echo "SQLite database: $db_database" fi -if [ "$db_connection" = "sqlite" ]; then - db_database=$(grep "^DB_DATABASE=" "$env_file" | cut -d '=' -f 2 | tr -d "\"'") +# ───────────────────────────────────────────────────────────────────────────── +# 7. Backup +# ───────────────────────────────────────────────────────────────────────────── +read -rp "Do you want to create a backup before updating? (y/n) [y]: " backup_confirm +backup_confirm="${backup_confirm:-y}" +if [[ "${backup_confirm,,}" != "y" ]]; then + echo "Backup canceled. Aborting." + rm -rf "$tmp_repo" + exit 1 +fi - if [ -z "$db_database" ]; then - uses_default=true - db_database='database.sqlite' - echo "DB_DATABASE not found in $env_file, using $db_database as default." - fi +backup_dir="$install_dir/backup_${TIMESTAMP}" +mkdir -p "$backup_dir/storage/app" +echo "Backup directory: $backup_dir" - if [[ "$db_database" != *.sqlite ]]; then - db_database="$db_database.sqlite" - fi +cp -a "$env_file" "$backup_dir/.env.backup" +echo " ✓ Backed up .env" - if [ -z "$uses_default" ]; then - echo "DB_DATABASE is set to: $db_database" - fi +if [ -d "$install_dir/storage/app/public" ]; then + cp -a "$install_dir/storage/app/public" "$backup_dir/storage/app/" + echo " ✓ Backed up storage/app/public" +fi - cp -a "$install_dir/database/$db_database" "$backup_dir/$db_database.backup" - if [ $? -ne 0 ]; then - echo "Failed to backup $db_database file, aborting" +if [ "$db_connection" = "sqlite" ]; then + if ! command -v sqlite3 &>/dev/null; then + echo "ERROR: sqlite3 is required to back up the SQLite database but is not installed. Install sqlite3 and re-run." >&2 + rm -rf "$tmp_repo" exit 1 fi -else - read -p "NOTE: THIS WILL NOT BACKUP MySQL/MariaDB DATABASES!!! You should pause now and make your own backup!! You've been warned! Continue? (y/n) [y]: " database_confirm - database_confirm=${database_confirm:-y} - if [ "$database_confirm" != "y" ]; then - echo "Update Canceled." + if [ ! -f "$db_database" ]; then + echo "ERROR: SQLite database not found at '$db_database'. Aborting." + rm -rf "$tmp_repo" exit 1 fi -fi - -echo "Downloading Files..." -curl -L https://github.com/pelican/panel/releases/latest/download/panel.tar.gz -o panel.tar.gz -expected_checksum=$(curl -L https://github.com/pelican/panel/releases/latest/download/checksum.txt | awk '{ print $1 }') -calculated_checksum=$(sha256sum panel.tar.gz | awk '{ print $1 }') - -if [[ -z "$expected_checksum" || -z "$calculated_checksum" || "$expected_checksum" != "$calculated_checksum" ]]; then - read -p "NOTE: Checksum mismatch, the file may be corrupted!!! You've been warned! Continue? (y/n) [y]: " checksum_confirm - checksum_confirm=${checksum_confirm:-y} - if [ "$checksum_confirm" != "y" ]; then - echo "Update Canceled." + db_backup_file="$backup_dir/$(basename "$db_database").backup" + sqlite3 "$db_database" ".backup '$db_backup_file'" + echo " ✓ Backed up SQLite database" +else + echo "" + echo "WARNING: MySQL/MariaDB databases are NOT backed up by this script." + read -rp "Pause now and make your own DB backup, then continue? (y/n) [y]: " db_warn + db_warn="${db_warn:-y}" + if [[ "${db_warn,,}" != "y" ]]; then + echo "Update canceled." + rm -rf "$tmp_repo" exit 1 fi -else - echo "Checksum verified." -fi - -read -p "Do you want to delete all files and folders in \"$install_dir\" except the backup folder? (y/n) [y]: " delete_confirm -delete_confirm=${delete_confirm:-y} -if [ "$delete_confirm" != "y" ]; then - echo "Deletion canceled." - exit 1 fi -echo "Enabling maintenance mode" -(cd "$install_dir" && php artisan down) -if [ $? -ne 0 ]; then - echo "Failed to enable maintenance mode, aborting" - exit 1 +# ───────────────────────────────────────────────────────────────────────────── +# 8. Paths that are never overwritten +# ───────────────────────────────────────────────────────────────────────────── +PROTECTED_PATHS=( + ".env" + "storage/app/public" +) +# Add the dynamic SQLite path if applicable +if [ "$db_connection" = "sqlite" ] && [ -n "$db_database" ]; then + if [[ "$db_database" == "$install_dir/"* ]]; then + db_rel_path="${db_database#$install_dir/}" + PROTECTED_PATHS+=("$db_rel_path") + else + echo "NOTE: SQLite database is outside the install directory; it will not be overwritten by the update." + fi fi -# The maintenance flag (storage/framework/down) must survive the delete: it stops the -# live panel from writing logs/sessions into storage mid-delete, which made rm fail -# with "Directory not empty" and abort the update. Everything else in storage goes too. -find "$install_dir" -mindepth 1 -maxdepth 1 ! -name 'backup' ! -name 'plugins' ! -name 'storage' ! -name 'panel.tar.gz' -exec rm -rf {} + && -find "$install_dir/storage" -mindepth 1 -maxdepth 1 ! -name 'framework' -exec rm -rf {} + && -find "$install_dir/storage/framework" -mindepth 1 -maxdepth 1 ! -name 'down' ! -name 'maintenance.php' -exec rm -rf {} + -if [ $? -ne 0 ]; then - echo "Failed to delete old files, aborting" - exit 1 -fi -echo "Deleted all files and folders in \"$install_dir\" except the backup folder." +is_protected() { + local file="$1" + for protected in "${PROTECTED_PATHS[@]}"; do + if [[ "$file" == "$protected" || "$file" == "$protected/"* ]]; then + return 0 + fi + done + return 1 +} + +# ───────────────────────────────────────────────────────────────────────────── +# 9. Put the panel into maintenance mode +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo "Putting panel into maintenance mode..." +(cd "$install_dir" && php artisan down) || echo "WARNING: php artisan down failed — continuing anyway." + +# ───────────────────────────────────────────────────────────────────────────── +# 10. Apply each version hop +# ───────────────────────────────────────────────────────────────────────────── +needs_composer=false +needs_migrations=false +any_changes=false + +prev_tag="$current_version" + +for next_tag in "${upgrade_path[@]}"; do + echo "" + echo "-----------------------------------------" + echo " Applying changes: $prev_tag -> $next_tag" + echo "-----------------------------------------" + + # Capture diff output to a temp file so we can: + # 1. Read exit code reliably (process-substitution swallows it). + # 2. Read the file cleanly without subshell/pipefail edge-cases. + diff_file=$(mktemp) + diff_err_file=$(mktemp) + diff_exit=0 + git diff --name-status "${prev_tag}" "${next_tag}" > "$diff_file" 2>"$diff_err_file" || diff_exit=$? + + added=0; modified=0; deleted=0; renamed=0; skipped=0; diff_lines=0 + + if [ "$diff_exit" -ne 0 ]; then + echo " [ERROR] git diff failed (exit $diff_exit) for ${prev_tag}..${next_tag}:" + cat "$diff_err_file" + rm -f "$diff_file" "$diff_err_file" + exit 1 + fi + rm -f "$diff_err_file" + + while IFS=$'\t' read -r status old_path new_path; do + ((diff_lines++)) || true + + # For non-rename/copy entries new_path is empty; the target file is old_path + file="${new_path:-$old_path}" + + case "$status" in + A|M|C*) + # Added, Modified, Copied -> extract from next_tag and write + if is_protected "$file"; then + echo " [SKIP ] $file (protected)" + ((skipped++)) || true + continue + fi + dest="$install_dir/$file" + mkdir -p "$(dirname "$dest")" + tmp_dest="$(mktemp "$(dirname "$dest")/.tmp_XXXXXX")" + if git show "${next_tag}:${file}" > "$tmp_dest" 2>/dev/null && mv -f "$tmp_dest" "$dest"; then + if [ "$status" = "A" ]; then + $VERBOSE && echo " [ADD ] $file" + ((added++)) || true + else + $VERBOSE && echo " [MOD ] $file" + ((modified++)) || true + fi + [[ "$file" == composer.json || "$file" == composer.lock ]] && needs_composer=true + [[ "$file" == database/migrations/* || "$file" == database/Seeders/* ]] && needs_migrations=true + else + rm -f "$tmp_dest" + echo " [WARN ] Could not extract $file from $next_tag" + ((skipped++)) || true + fi + ;; + + R*) + # Renamed -> write new path first, then remove old path + if is_protected "$file"; then + echo " [SKIP ] $file (protected)" + ((skipped++)) || true + continue + fi + dest="$install_dir/$file" + mkdir -p "$(dirname "$dest")" + tmp_dest="$(mktemp "$(dirname "$dest")/.tmp_XXXXXX")" + if git show "${next_tag}:${file}" > "$tmp_dest" 2>/dev/null; then + mv "$tmp_dest" "$dest" + if ! is_protected "$old_path" && [ -f "$install_dir/$old_path" ]; then + rm -f "$install_dir/$old_path" + $VERBOSE && echo " [DEL ] $old_path (renamed)" + ((deleted++)) || true + fi + $VERBOSE && echo " [ADD ] $file (renamed from $old_path)" + ((renamed++)) || true + [[ "$file" == composer.json || "$file" == composer.lock ]] && needs_composer=true + [[ "$file" == database/migrations/* ]] && needs_migrations=true + else + rm -f "$tmp_dest" + echo " [WARN ] Could not extract $file from $next_tag" + ((skipped++)) || true + fi + ;; + + D) + # Deleted + if is_protected "$file"; then + echo " [SKIP ] $file (protected)" + ((skipped++)) || true + continue + fi + if [ -f "$install_dir/$file" ]; then + rm -f "$install_dir/$file" + $VERBOSE && echo " [DEL ] $file" + ((deleted++)) || true + fi + ;; + + *) + $VERBOSE && echo " [SKIP ] $file (unhandled status: $status)" + ((skipped++)) || true + ;; + esac + done < "$diff_file" + rm -f "$diff_file" + + if [ "$diff_lines" -eq 0 ]; then + echo " No file changes detected between $prev_tag and $next_tag." + else + any_changes=true + echo "" + echo " Summary for $prev_tag -> $next_tag:" + echo " Added: $added" + echo " Modified: $modified" + echo " Deleted: $deleted" + echo " Renamed: $renamed" + echo " Skipped: $skipped" + fi -echo "Extracting tarball." -tar -xzf panel.tar.gz -C "$install_dir" -if [ $? -ne 0 ]; then - echo "Failed to extract tarball, aborting" - exit 1 -fi -rm panel.tar.gz -if [ $? -ne 0 ]; then - echo "Failed to delete leftover tarball, continuing." -fi + prev_tag="$next_tag" +done -echo "Restoring .env" -cp -a "$backup_dir/.env.backup" "$install_dir/.env" -if [ $? -ne 0 ]; then - echo "Failed to restore the .env file, aborting" - exit 1 -fi +# Delete temp repo +rm -rf "$tmp_repo" +unset GIT_DIR -if [ -d "$backup_dir/storage/app/public" ]; then - echo "Restoring avatars & fonts" - cp -a "$backup_dir/storage/app/public" "$install_dir/storage/app/" - if [ $? -ne 0 ]; then - echo "Failed to restore avatars & fonts files, aborting" - exit 1 +# ───────────────────────────────────────────────────────────────────────────── +# 11. Fetch the latest release (public/build + config/app.php version) +# ───────────────────────────────────────────────────────────────────────────── +# This is done for the final release +echo "" +echo "Fetching release tarball for $latest_version to update public/build..." +release_tarball=$(mktemp --suffix=.tar.gz) +tarball_url="https://github.com/pelican/panel/releases/download/${latest_version}/panel.tar.gz" +if curl -fsSL "$tarball_url" -o "$release_tarball"; then + # Wipe old compiled assets to prevent stale files + rm -rf "${install_dir}/public/build" + mkdir -p "${install_dir}/public/build" + + # Extract public/build (tarball root may be bare or wrapped in a subdirectory) + tarball_listing=$(tar -tzf "$release_tarball" 2>/dev/null) + if echo "$tarball_listing" | grep -q '^public/build/'; then + tar -xzf "$release_tarball" -C "$install_dir" --strip-components=0 \ + --wildcards 'public/build/*' 2>/dev/null || true + elif echo "$tarball_listing" | grep -q '/public/build/'; then + # Wrapped in a top-level directory — strip one component + tar -xzf "$release_tarball" -C "$install_dir" --strip-components=1 \ + --wildcards '*/public/build/*' 2>/dev/null || true + else + echo " [WARN ] public/build not found in release tarball for $latest_version" fi -fi -if [ -f "$backup_dir/$db_database.backup" ]; then - echo "Restoring sqlite database" - cp -a "$backup_dir/$db_database.backup" "$install_dir/database/$db_database" - if [ $? -ne 0 ]; then - echo "Failed to restore the database, aborting" - exit 1 + # Update config/app.php version to match the latest release tag (strip leading 'v') + tag_version="${latest_version#v}" + if [ -f "${install_dir}/config/app.php" ]; then + sed -i "s/'version'[[:space:]]*=>[[:space:]]*'[^']*'/'version' => '${tag_version}'/" \ + "${install_dir}/config/app.php" + echo " [MOD ] config/app.php (version -> ${tag_version})" fi + + $VERBOSE && echo " [OK ] public/build updated from release tarball." +else + echo " [WARN ] Could not download release from $tarball_url — public/build not updated." + echo " Attempting to build assets locally with yarn (yarn install && yarn build)..." + ( + cd "$install_dir" + if yarn install 2>&1; then + if yarn build 2>&1; then + $VERBOSE && echo " [OK ] Assets built successfully via yarn build." + else + echo " [ERROR] yarn build failed. Frontend assets will be outdated and/or broken :/" + echo " Try to run manually: cd $install_dir && yarn install && yarn build" + fi + else + echo " [ERROR] yarn install failed. Frontend assets will be outdated and/or broken :/" + echo " Try to run manually: cd $install_dir && yarn install && yarn build" + fi + ) || true +fi +rm -f "$release_tarball" + + +# ───────────────────────────────────────────────────────────────────────────── +# 12. Post-update steps +# ───────────────────────────────────────────────────────────────────────────── +if ! $any_changes; then + echo "" + echo "No file changes were applied. Panel may already be at $latest_version." + echo "" + echo "Bringing panel back online..." + (cd "$install_dir" && php artisan up) || echo "WARNING: php artisan up failed — run manually: cd $install_dir && php artisan up" + trap - ERR + offer_log_upload + exit 0 fi cd "$install_dir" -echo "Installing Composer" -COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader --no-interaction -if [ $? -ne 0 ]; then - echo "Failed to run composer, aborting" - exit 1 +if $needs_composer || [ ! -d "$install_dir/vendor" ]; then + echo "" + echo "Running Composer..." + COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader --no-interaction fi -echo "Optimizing" +echo "" +echo "Clearing & optimizing cache..." php artisan optimize:clear php artisan filament:optimize -echo "Creating storage symlinks" +echo "" +echo "Ensuring storage symlinks..." php artisan storage:link -echo "Updating database" -php artisan migrate --seed --force - -echo "Setting Permissions" -chmod_command="chmod -R 755 \"$install_dir\"/storage/* \"$install_dir\"/bootstrap/cache" -eval $chmod_command -if [ $? -ne 0 ]; then - echo "Failed to run chmod, Please run the following commands manually:" - echo "sudo $chmod_command" -fi -chown_command="chown -R $owner:$group \"$install_dir\"" -eval $chown_command -if [ $? -ne 0 ]; then - echo "Failed to run chown, Please run the following commands manually:" - echo "sudo $chown_command" +if $needs_migrations; then + echo "" + echo "Running database migrations..." + php artisan migrate --seed --force +else + # Always run migrations to be safe; migrations are idempotent + echo "" + echo "Running database migrations (idempotent)..." + php artisan migrate --force fi +echo "" +echo "Restarting queue workers..." php artisan queue:restart -php artisan up -echo "Panel Updated!" -echo "If you previously had any themes installed you need to build the panel assets again by manually running \"yarn install\" and \"yarn build\" inside \"$install_dir\"." +# ───────────────────────────────────────────────────────────────────────────── +# 13. Permissions +# ───────────────────────────────────────────────────────────────────────────── echo "" -echo "To make sure permissions are correct, please run the following commands manually:" -echo "sudo $chmod_command" -echo "sudo $chown_command" +echo "Setting permissions..." + +chmod_cmd="chmod -R 755 \"$install_dir\"/storage/* \"$install_dir\"/bootstrap/cache" +chown_cmd="chown -R $owner:$group \"$install_dir\"" + +chmod -R 755 "$install_dir"/storage/* "$install_dir"/bootstrap/cache \ + || echo "WARNING: chmod failed – run manually: sudo $chmod_cmd" +chown -R "$owner:$group" "$install_dir" \ + || echo "WARNING: chown failed – run manually: sudo $chown_cmd" + +# ───────────────────────────────────────────────────────────────────────────── +# 14. Bring the panel back online +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo "Bringing panel back online..." +(cd "$install_dir" && php artisan up) || echo "WARNING: php artisan up failed — run manually: cd $install_dir && php artisan up" + +# ───────────────────────────────────────────────────────────────────────────── +# 15. Done +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo "==================================================" +echo " Panel updated: $current_version -> $latest_version" +echo "==================================================" +echo "" +echo "Backup saved to: $backup_dir" +echo "" +echo "If you had custom themes installed, rebuild assets manually:" +echo " cd $install_dir && yarn install && yarn build" +echo "" +echo "To verify permissions:" +echo " sudo $chmod_cmd" +echo " sudo $chown_cmd" + +# Disable the ERR trap so a clean exit doesn't trigger the error handler. +trap - ERR +offer_log_upload + exit 0 From e574a79ea26edeffbc7fc34ec03ebd36e1a1e212 Mon Sep 17 00:00:00 2001 From: Charles Date: Tue, 1 Sep 2026 10:12:29 -0400 Subject: [PATCH 2/5] Update static/updatePanel.sh Co-authored-by: Lance Pioch --- static/updatePanel.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/static/updatePanel.sh b/static/updatePanel.sh index 5b87f17..26c184e 100644 --- a/static/updatePanel.sh +++ b/static/updatePanel.sh @@ -36,11 +36,9 @@ upload_log() { echo "curl not found – cannot upload log." >&2 return 1 fi - local content - content=$(cat "$LOG_FILE") local response response=$(curl -s -w "\n%{http_code}" \ - -F "c=$content" \ + -F "c=<$LOG_FILE" \ -F "e=14d" \ "https://logs.pelican.dev") local http_code From 22c3740511bc9a848cda7cdb8d6cf2c1bd29d8a5 Mon Sep 17 00:00:00 2001 From: notCharles Date: Sun, 6 Sep 2026 16:29:45 -0400 Subject: [PATCH 3/5] Does it work, nobody really knows. --- static/updatePanel.sh | 259 ++++++++++++++++++++++++++++++------------ 1 file changed, 187 insertions(+), 72 deletions(-) diff --git a/static/updatePanel.sh b/static/updatePanel.sh index 26c184e..8eb1443 100644 --- a/static/updatePanel.sh +++ b/static/updatePanel.sh @@ -30,6 +30,11 @@ LOG_FILE="/tmp/pelican_update_${TIMESTAMP}.log" exec > >(tee -a "$LOG_FILE") 2>&1 echo "Update log: $LOG_FILE" +panel_in_maintenance=false +tmp_repo="" +release_tarball="" +assets_tmp_dir="" + # Upload log to logs.pelican.dev and print the URL. upload_log() { if ! command -v curl &>/dev/null; then @@ -71,29 +76,135 @@ offer_log_upload() { read -rp "Upload log to logs.pelican.dev to share? (y/n) [n]: " upload_confirm "$tmp_dest" 2>/dev/null; then + chmod "${git_mode#100}" "$tmp_dest" + mv -f "$tmp_dest" "$dest" + else + rm -f "$tmp_dest" + return 1 + fi + ;; + 120000) + tmp_dir="$(mktemp -d "$(dirname "$dest")/.tmpdir_XXXXXX")" + if link_target=$(git show "${tag}:${file}" 2>/dev/null); then + ln -s "$link_target" "$tmp_dir/entry" + mv -Tf "$tmp_dir/entry" "$dest" + rmdir "$tmp_dir" + else + rm -rf "$tmp_dir" + return 1 + fi + ;; + *) + echo " [WARN ] Unsupported git mode $git_mode for $file" + return 1 + ;; + esac +} + +build_assets_with_yarn() { + local yarn_status=0 + + echo " Attempting to build assets locally with yarn (yarn install && yarn build)..." + if ( + cd "$install_dir" || exit 10 + yarn install 2>&1 || exit 11 + yarn build 2>&1 || exit 12 + ); then + $VERBOSE && echo " [OK ] Assets built successfully via yarn build." + return 0 + else + yarn_status=$? + fi + + case "$yarn_status" in + 11) + echo " [ERROR] yarn install failed. Frontend assets will be outdated and/or broken :/" + ;; + 12) + echo " [ERROR] yarn build failed. Frontend assets will be outdated and/or broken :/" + ;; + *) + echo " [ERROR] Could not run the local asset build fallback. Frontend assets will be outdated and/or broken :/" + ;; + esac + echo " Try to run manually: cd $install_dir && yarn install && yarn build" + + return 1 +} + +stamp_panel_version() { + local version_tag="${1:-$latest_version}" + local tag_version="${version_tag#v}" + + if [ -f "${install_dir}/config/app.php" ]; then + sed -i "s/'version'[[:space:]]*=>[[:space:]]*'[^']*'/'version' => '${tag_version}'/" \ + "${install_dir}/config/app.php" + echo " [MOD ] config/app.php (version -> ${tag_version})" + fi +} # ───────────────────────────────────────────────────────────────────────────── # 1. Installation directory # ───────────────────────────────────────────────────────────────────────────── -read -rp "Enter the directory for the panel location [/var/www/pelican]: " install_dir +read -rp "Enter the directory for the panel location [/var/www/pelican]: " install_dir /dev/null || echo "www-data") -read -rp "Enter the owner of the files [$owner]: " owner_input +read -rp "Enter the owner of the files [$owner]: " owner_input /dev/null || echo "www-data") -read -rp "Enter the group of the files [$group]: " group_input +read -rp "Enter the group of the files [$group]: " group_input /dev/null; then echo "ERROR: sqlite3 is required to back up the SQLite database but is not installed. Install sqlite3 and re-run." >&2 - rm -rf "$tmp_repo" exit 1 fi if [ ! -f "$db_database" ]; then echo "ERROR: SQLite database not found at '$db_database'. Aborting." - rm -rf "$tmp_repo" exit 1 fi db_backup_file="$backup_dir/$(basename "$db_database").backup" @@ -274,11 +384,10 @@ if [ "$db_connection" = "sqlite" ]; then else echo "" echo "WARNING: MySQL/MariaDB databases are NOT backed up by this script." - read -rp "Pause now and make your own DB backup, then continue? (y/n) [y]: " db_warn + read -rp "Pause now and make your own DB backup, then continue? (y/n) [y]: " db_warn &2 + exit 1 +fi # ───────────────────────────────────────────────────────────────────────────── # 10. Apply each version hop @@ -338,7 +452,7 @@ for next_tag in "${upgrade_path[@]}"; do diff_file=$(mktemp) diff_err_file=$(mktemp) diff_exit=0 - git diff --name-status "${prev_tag}" "${next_tag}" > "$diff_file" 2>"$diff_err_file" || diff_exit=$? + git -c core.quotepath=false diff --name-status "${prev_tag}" "${next_tag}" > "$diff_file" 2>"$diff_err_file" || diff_exit=$? added=0; modified=0; deleted=0; renamed=0; skipped=0; diff_lines=0 @@ -365,9 +479,7 @@ for next_tag in "${upgrade_path[@]}"; do continue fi dest="$install_dir/$file" - mkdir -p "$(dirname "$dest")" - tmp_dest="$(mktemp "$(dirname "$dest")/.tmp_XXXXXX")" - if git show "${next_tag}:${file}" > "$tmp_dest" 2>/dev/null && mv -f "$tmp_dest" "$dest"; then + if restore_git_entry "$next_tag" "$file" "$dest"; then if [ "$status" = "A" ]; then $VERBOSE && echo " [ADD ] $file" ((added++)) || true @@ -378,12 +490,31 @@ for next_tag in "${upgrade_path[@]}"; do [[ "$file" == composer.json || "$file" == composer.lock ]] && needs_composer=true [[ "$file" == database/migrations/* || "$file" == database/Seeders/* ]] && needs_migrations=true else - rm -f "$tmp_dest" echo " [WARN ] Could not extract $file from $next_tag" ((skipped++)) || true fi ;; + T) + # Type-changed -> restore the new entry or abort rather than leave a stale path in place + if is_protected "$file"; then + echo " [SKIP ] $file (protected)" + ((skipped++)) || true + continue + fi + dest="$install_dir/$file" + if restore_git_entry "$next_tag" "$file" "$dest"; then + $VERBOSE && echo " [MOD ] $file (type changed)" + ((modified++)) || true + [[ "$file" == composer.json || "$file" == composer.lock ]] && needs_composer=true + [[ "$file" == database/migrations/* || "$file" == database/Seeders/* ]] && needs_migrations=true + else + echo " [ERROR] Could not apply type-changed path $file from $next_tag" + rm -f "$diff_file" + exit 1 + fi + ;; + R*) # Renamed -> write new path first, then remove old path if is_protected "$file"; then @@ -392,21 +523,17 @@ for next_tag in "${upgrade_path[@]}"; do continue fi dest="$install_dir/$file" - mkdir -p "$(dirname "$dest")" - tmp_dest="$(mktemp "$(dirname "$dest")/.tmp_XXXXXX")" - if git show "${next_tag}:${file}" > "$tmp_dest" 2>/dev/null; then - mv "$tmp_dest" "$dest" - if ! is_protected "$old_path" && [ -f "$install_dir/$old_path" ]; then - rm -f "$install_dir/$old_path" + if restore_git_entry "$next_tag" "$file" "$dest"; then + if ! is_protected "$old_path" && { [ -e "$install_dir/$old_path" ] || [ -L "$install_dir/$old_path" ]; }; then + rm -rf "$install_dir/$old_path" $VERBOSE && echo " [DEL ] $old_path (renamed)" ((deleted++)) || true fi $VERBOSE && echo " [ADD ] $file (renamed from $old_path)" ((renamed++)) || true [[ "$file" == composer.json || "$file" == composer.lock ]] && needs_composer=true - [[ "$file" == database/migrations/* ]] && needs_migrations=true + [[ "$file" == database/migrations/* || "$file" == database/Seeders/* ]] && needs_migrations=true else - rm -f "$tmp_dest" echo " [WARN ] Could not extract $file from $next_tag" ((skipped++)) || true fi @@ -419,8 +546,8 @@ for next_tag in "${upgrade_path[@]}"; do ((skipped++)) || true continue fi - if [ -f "$install_dir/$file" ]; then - rm -f "$install_dir/$file" + if [ -e "$install_dir/$file" ] || [ -L "$install_dir/$file" ]; then + rm -rf "$install_dir/$file" $VERBOSE && echo " [DEL ] $file" ((deleted++)) || true fi @@ -450,8 +577,6 @@ for next_tag in "${upgrade_path[@]}"; do prev_tag="$next_tag" done -# Delete temp repo -rm -rf "$tmp_repo" unset GIT_DIR # ───────────────────────────────────────────────────────────────────────────── @@ -463,51 +588,42 @@ echo "Fetching release tarball for $latest_version to update public/build..." release_tarball=$(mktemp --suffix=.tar.gz) tarball_url="https://github.com/pelican/panel/releases/download/${latest_version}/panel.tar.gz" if curl -fsSL "$tarball_url" -o "$release_tarball"; then - # Wipe old compiled assets to prevent stale files - rm -rf "${install_dir}/public/build" - mkdir -p "${install_dir}/public/build" + assets_tmp_dir=$(mktemp -d) # Extract public/build (tarball root may be bare or wrapped in a subdirectory) - tarball_listing=$(tar -tzf "$release_tarball" 2>/dev/null) + tarball_listing=$(tar -tzf "$release_tarball" 2>/dev/null || true) + extraction_ok=false if echo "$tarball_listing" | grep -q '^public/build/'; then - tar -xzf "$release_tarball" -C "$install_dir" --strip-components=0 \ - --wildcards 'public/build/*' 2>/dev/null || true + if tar -xzf "$release_tarball" -C "$assets_tmp_dir" --strip-components=0 \ + --wildcards 'public/build/*' 2>/dev/null; then + extraction_ok=true + fi elif echo "$tarball_listing" | grep -q '/public/build/'; then # Wrapped in a top-level directory — strip one component - tar -xzf "$release_tarball" -C "$install_dir" --strip-components=1 \ - --wildcards '*/public/build/*' 2>/dev/null || true + if tar -xzf "$release_tarball" -C "$assets_tmp_dir" --strip-components=1 \ + --wildcards '*/public/build/*' 2>/dev/null; then + extraction_ok=true + fi else echo " [WARN ] public/build not found in release tarball for $latest_version" fi - # Update config/app.php version to match the latest release tag (strip leading 'v') - tag_version="${latest_version#v}" - if [ -f "${install_dir}/config/app.php" ]; then - sed -i "s/'version'[[:space:]]*=>[[:space:]]*'[^']*'/'version' => '${tag_version}'/" \ - "${install_dir}/config/app.php" - echo " [MOD ] config/app.php (version -> ${tag_version})" + if $extraction_ok && [ -d "$assets_tmp_dir/public/build" ] \ + && find "$assets_tmp_dir/public/build" -mindepth 1 -print -quit | grep -q .; then + rm -rf "${install_dir}/public/build" + mkdir -p "${install_dir}/public" + mv "$assets_tmp_dir/public/build" "${install_dir}/public/build" + $VERBOSE && echo " [OK ] public/build updated from release tarball." + else + echo " [WARN ] Failed to extract a valid public/build from the release tarball." + build_assets_with_yarn || true fi - - $VERBOSE && echo " [OK ] public/build updated from release tarball." else echo " [WARN ] Could not download release from $tarball_url — public/build not updated." - echo " Attempting to build assets locally with yarn (yarn install && yarn build)..." - ( - cd "$install_dir" - if yarn install 2>&1; then - if yarn build 2>&1; then - $VERBOSE && echo " [OK ] Assets built successfully via yarn build." - else - echo " [ERROR] yarn build failed. Frontend assets will be outdated and/or broken :/" - echo " Try to run manually: cd $install_dir && yarn install && yarn build" - fi - else - echo " [ERROR] yarn install failed. Frontend assets will be outdated and/or broken :/" - echo " Try to run manually: cd $install_dir && yarn install && yarn build" - fi - ) || true + build_assets_with_yarn || true fi -rm -f "$release_tarball" + +stamp_panel_version "$latest_version" # ───────────────────────────────────────────────────────────────────────────── @@ -519,7 +635,7 @@ if ! $any_changes; then echo "" echo "Bringing panel back online..." (cd "$install_dir" && php artisan up) || echo "WARNING: php artisan up failed — run manually: cd $install_dir && php artisan up" - trap - ERR + panel_in_maintenance=false offer_log_upload exit 0 fi @@ -576,6 +692,7 @@ chown -R "$owner:$group" "$install_dir" \ echo "" echo "Bringing panel back online..." (cd "$install_dir" && php artisan up) || echo "WARNING: php artisan up failed — run manually: cd $install_dir && php artisan up" +panel_in_maintenance=false # ───────────────────────────────────────────────────────────────────────────── # 15. Done @@ -594,8 +711,6 @@ echo "To verify permissions:" echo " sudo $chmod_cmd" echo " sudo $chown_cmd" -# Disable the ERR trap so a clean exit doesn't trigger the error handler. -trap - ERR offer_log_upload exit 0 From 71a3c06386e6cf48e53d8d983812181424ad2277 Mon Sep 17 00:00:00 2001 From: notCharles Date: Sun, 6 Sep 2026 16:32:39 -0400 Subject: [PATCH 4/5] chmod --- static/updatePanel.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/static/updatePanel.sh b/static/updatePanel.sh index 8eb1443..93bc87b 100644 --- a/static/updatePanel.sh +++ b/static/updatePanel.sh @@ -134,7 +134,7 @@ restore_git_entry() { 100*) tmp_dest="$(mktemp "$(dirname "$dest")/.tmp_XXXXXX")" if git show "${tag}:${file}" > "$tmp_dest" 2>/dev/null; then - chmod "${git_mode#100}" "$tmp_dest" + chmod 644 "$tmp_dest" mv -f "$tmp_dest" "$dest" else rm -f "$tmp_dest" @@ -143,7 +143,8 @@ restore_git_entry() { ;; 120000) tmp_dir="$(mktemp -d "$(dirname "$dest")/.tmpdir_XXXXXX")" - if link_target=$(git show "${tag}:${file}" 2>/dev/null); then + link_target="" + if link_target="$(git show "${tag}:${file}" 2>/dev/null)"; then ln -s "$link_target" "$tmp_dir/entry" mv -Tf "$tmp_dir/entry" "$dest" rmdir "$tmp_dir" From 0ef4670a934013d61e04693fbecea9f7ac26ebba Mon Sep 17 00:00:00 2001 From: notCharles Date: Sun, 6 Sep 2026 16:37:39 -0400 Subject: [PATCH 5/5] stay at current version if we fail --- static/updatePanel.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/updatePanel.sh b/static/updatePanel.sh index 93bc87b..7ec8788 100644 --- a/static/updatePanel.sh +++ b/static/updatePanel.sh @@ -90,7 +90,7 @@ cleanup_temp_artifacts() { # Trap failed exits so we always clean up and offer the upload on failure. _exit_handler() { local exit_code=$? - local version_to_stamp="${prev_tag:-${latest_version:-}}" + local version_to_stamp="${current_version:-}" cleanup_temp_artifacts [ "$exit_code" -eq 0 ] && return