update.sh 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. #!/usr/bin/env bash
  2. set -Eeuo pipefail
  3. INSTALL_DIR="${INSTALL_DIR:-/opt/bambuddy}"
  4. SERVICE_NAME="${SERVICE_NAME:-bambuddy}"
  5. BRANCH="${BRANCH:-}"
  6. VENV_PIP="${VENV_PIP:-$INSTALL_DIR/venv/bin/pip}"
  7. FRONTEND_DIR="${FRONTEND_DIR:-$INSTALL_DIR/frontend}"
  8. BACKUP_DIR="${BACKUP_DIR:-$INSTALL_DIR/backups}"
  9. BAMBUDDY_API_URL="${BAMBUDDY_API_URL:-http://127.0.0.1:8000/api/v1}"
  10. BAMBUDDY_API_KEY="${BAMBUDDY_API_KEY:-}"
  11. BACKUP_MODE="${BACKUP_MODE:-auto}" # auto|require|skip
  12. BACKUP_KEEP_COUNT=5
  13. FORCE="${FORCE:-0}"
  14. SERVICE_STOPPED=0
  15. CODE_UPDATED=0
  16. old_commit=""
  17. log() {
  18. printf '[bambuddy-update] %s\n' "$*"
  19. }
  20. warn() {
  21. printf '[bambuddy-update] WARNING: %s\n' "$*" >&2
  22. }
  23. die() {
  24. printf '[bambuddy-update] ERROR: %s\n' "$*" >&2
  25. exit 1
  26. }
  27. require_cmd() {
  28. command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
  29. }
  30. cleanup_old_backups() {
  31. local -a backup_files
  32. local max_count="$1"
  33. [ "$max_count" -gt 0 ] || return 0
  34. mapfile -t backup_files < <(ls -1t "$BACKUP_DIR"/bambuddy-backup-*.zip 2>/dev/null || true)
  35. if [ "${#backup_files[@]}" -le "$max_count" ]; then
  36. return 0
  37. fi
  38. for old_file in "${backup_files[@]:$max_count}"; do
  39. rm -f "$old_file"
  40. done
  41. log "Pruned old backups, kept newest $max_count file(s)"
  42. }
  43. on_error() {
  44. local exit_code="$1"
  45. if [ "$SERVICE_STOPPED" -eq 1 ]; then
  46. if [ "$CODE_UPDATED" -eq 1 ] && [ -n "$old_commit" ]; then
  47. warn "Update failed after code change, attempting rollback to $old_commit"
  48. git reset --hard "$old_commit" || warn "Rollback reset failed"
  49. fi
  50. warn "Update failed, attempting to restart service: $SERVICE_NAME"
  51. systemctl start "$SERVICE_NAME" || true
  52. fi
  53. exit "$exit_code"
  54. }
  55. trap 'on_error $?' ERR
  56. create_backup() {
  57. local ts backup_file
  58. local -a auth_args=()
  59. if [ "$BACKUP_MODE" = "skip" ]; then
  60. log "Skipping backup (BACKUP_MODE=skip)"
  61. return 0
  62. fi
  63. if ! systemctl is-active --quiet "$SERVICE_NAME"; then
  64. if [ "$BACKUP_MODE" = "require" ]; then
  65. die "Service is not running; cannot call built-in backup API."
  66. fi
  67. warn "Service is not running; skipping built-in backup API call."
  68. return 0
  69. fi
  70. mkdir -p "$BACKUP_DIR"
  71. ts="$(date +%Y%m%d-%H%M%S)"
  72. backup_file="$BACKUP_DIR/bambuddy-backup-$ts.zip"
  73. [ -n "$BAMBUDDY_API_KEY" ] && auth_args=(-H "X-API-Key: $BAMBUDDY_API_KEY")
  74. log "Creating built-in backup via API: $backup_file"
  75. if curl --silent --show-error --fail --location \
  76. --connect-timeout 5 --max-time 900 \
  77. "${auth_args[@]}" \
  78. "$BAMBUDDY_API_URL/settings/backup" \
  79. --output "$backup_file"; then
  80. log "Backup created successfully"
  81. cleanup_old_backups "$BACKUP_KEEP_COUNT"
  82. return 0
  83. fi
  84. rm -f "$backup_file"
  85. if [ "$BACKUP_MODE" = "require" ]; then
  86. die "Built-in backup API call failed (BACKUP_MODE=require)."
  87. fi
  88. warn "Built-in backup API call failed. Continuing because BACKUP_MODE=auto."
  89. }
  90. [ "${EUID:-$(id -u)}" -eq 0 ] || die "Run as root (or with sudo)."
  91. case "$BACKUP_MODE" in
  92. auto|require|skip) ;;
  93. *) die "Invalid BACKUP_MODE '$BACKUP_MODE' (expected: auto, require, skip)." ;;
  94. esac
  95. require_cmd git
  96. require_cmd systemctl
  97. require_cmd curl
  98. [ -d "$INSTALL_DIR" ] || die "Install directory not found: $INSTALL_DIR"
  99. cd "$INSTALL_DIR"
  100. if [ ! -d .git ]; then
  101. cat >&2 <<EOF
  102. [bambuddy-update] ERROR: No .git directory found in $INSTALL_DIR.
  103. This update script requires a git-based install. If you installed by
  104. downloading a ZIP or tarball from GitHub, reinstall from scratch:
  105. 1. Back up your data:
  106. sudo systemctl stop $SERVICE_NAME
  107. sudo tar czf ~/bambuddy-backup.tgz -C $INSTALL_DIR \\
  108. data bambuddy.db bambuddy.db-shm bambuddy.db-wal \\
  109. virtual_printer archive projects icons .env 2>/dev/null || true
  110. 2. Remove the old install and reinstall via install.sh:
  111. sudo rm -rf $INSTALL_DIR
  112. curl -fsSL https://raw.githubusercontent.com/maziggy/bambuddy/main/install/install.sh \\
  113. -o /tmp/install.sh && sudo bash /tmp/install.sh --path $INSTALL_DIR
  114. 3. Restore your data:
  115. sudo systemctl stop $SERVICE_NAME
  116. sudo tar xzf ~/bambuddy-backup.tgz -C $INSTALL_DIR
  117. sudo systemctl start $SERVICE_NAME
  118. EOF
  119. exit 1
  120. fi
  121. if [ -z "$BRANCH" ]; then
  122. BRANCH="$(git rev-parse --abbrev-ref HEAD)"
  123. [ "$BRANCH" = "HEAD" ] && BRANCH="main"
  124. fi
  125. load_state="$(systemctl show "$SERVICE_NAME" --property=LoadState --value 2>/dev/null || true)"
  126. if [ -z "$load_state" ] || [ "$load_state" = "not-found" ]; then
  127. die "Service not found: ${SERVICE_NAME}.service"
  128. fi
  129. old_commit="$(git rev-parse --short HEAD || true)"
  130. log "Fetching latest code from origin/$BRANCH"
  131. git fetch --prune origin
  132. remote_commit="$(git rev-parse --short "origin/$BRANCH" || true)"
  133. log "Current commit: ${old_commit:-unknown}"
  134. log "Remote commit: ${remote_commit:-unknown}"
  135. if git diff --quiet HEAD "origin/$BRANCH"; then
  136. log "You are already running the latest version of Bambuddy."
  137. read -r -p "Do you want to run the update process anyway? [y/N]: " run_anyway
  138. case "${run_anyway:-}" in
  139. y|Y|yes|YES) ;;
  140. *) exit 0 ;;
  141. esac
  142. else
  143. read -r -p "An update for Bambuddy is available. Install now? [y/N]: " install_now
  144. case "${install_now:-}" in
  145. y|Y|yes|YES) ;;
  146. *) exit 0 ;;
  147. esac
  148. fi
  149. if [ -n "$(git status --porcelain)" ]; then
  150. if [ "$FORCE" != "1" ]; then
  151. read -r -p "Local edits were detected in your installation. Updating now will overwrite those edits. Continue? [y/N]: " answer
  152. case "${answer:-}" in
  153. y|Y|yes|YES) ;;
  154. *) die "Update cancelled by user." ;;
  155. esac
  156. else
  157. warn "Proceeding without prompt because FORCE=1."
  158. fi
  159. fi
  160. create_backup
  161. log "Stopping service: $SERVICE_NAME"
  162. systemctl stop "$SERVICE_NAME"
  163. SERVICE_STOPPED=1
  164. log "Updating code to origin/$BRANCH"
  165. git reset --hard "origin/$BRANCH"
  166. CODE_UPDATED=1
  167. if [ -x "$VENV_PIP" ] && [ -f requirements.txt ]; then
  168. log "Updating Python dependencies"
  169. "$VENV_PIP" install -r requirements.txt
  170. else
  171. warn "Skipping Python dependency update (venv pip or requirements.txt missing)."
  172. fi
  173. if [ -f "$FRONTEND_DIR/package.json" ]; then
  174. if command -v npm >/dev/null 2>&1; then
  175. log "Building frontend"
  176. (
  177. cd "$FRONTEND_DIR"
  178. npm ci
  179. npm run build
  180. )
  181. else
  182. warn "Skipping frontend build (npm not installed)."
  183. fi
  184. else
  185. warn "Skipping frontend build (frontend/package.json not found)."
  186. fi
  187. log "Starting service: $SERVICE_NAME"
  188. systemctl start "$SERVICE_NAME"
  189. SERVICE_STOPPED=0
  190. systemctl --no-pager --lines=8 status "$SERVICE_NAME"
  191. new_commit="$(git rev-parse --short HEAD || true)"
  192. log "Update complete: ${old_commit:-unknown} -> ${new_commit:-unknown}"