#!/usr/bin/env bash set -euo pipefail if [[ -z "${BASH_VERSION:-}" ]]; then echo "Error: run this script with bash (example: bash deploy_synology.sh)." >&2 exit 1 fi # ManeshTrader quick deploy helper for Synology Docker host. # Defaults are set for the target provided by the user. ENV_FILE=".env.deploy_synology" if [[ -f "$ENV_FILE" ]]; then # shellcheck disable=SC1090 set -a; source "$ENV_FILE"; set +a fi REMOTE_USER="${REMOTE_USER:-mbrucedogs}" REMOTE_HOST="${REMOTE_HOST:-192.168.1.128}" REMOTE_PORT="${REMOTE_PORT:-25}" REMOTE_BASE="${REMOTE_BASE:-/volume1/docker/maneshtrader}" LOCAL_BASE="${LOCAL_BASE:-web/src}" SYNO_PASSWORD="${SYNO_PASSWORD:-}" CONTAINER_NAME="${CONTAINER_NAME:-}" SUDO_PASSWORD="${SUDO_PASSWORD:-${SYNO_PASSWORD:-}}" SUDO_MODE="${SUDO_MODE:-auto}" # auto|always|never SSH_PASS_WARNING_SHOWN="0" MODE="bind" # bind: upload + restart; image: upload + rebuild NO_RESTART="0" DRY_RUN="0" DEFAULT_FILES=( "web/src/web_core/ui/help_content.py" "web/src/PRD.md" "web/src/ONBOARDING.md" ) FILES=("${DEFAULT_FILES[@]}") usage() { cat <<'EOF' Usage: ./deploy_synology.sh [options] Options: --mode bind|image Deploy mode (default: bind) --files "a b c" Space-separated file list to upload --recent-minutes N Upload files under web/src modified in last N minutes --git-hours N Upload files changed in last N hours (git history + working tree) --env-file PATH Load environment variables from file (default: .env.deploy_synology if present) --sudo-mode MODE Docker privilege mode: auto|always|never (default: auto) --no-restart Upload only; skip remote docker restart/rebuild --dry-run Print actions but do not execute -h, --help Show help Environment overrides: REMOTE_USER, REMOTE_HOST, REMOTE_PORT, REMOTE_BASE, LOCAL_BASE, SYNO_PASSWORD, CONTAINER_NAME, SUDO_PASSWORD, SUDO_MODE Examples: ./deploy_synology.sh ./deploy_synology.sh --mode image ./deploy_synology.sh --recent-minutes 120 ./deploy_synology.sh --git-hours 2 ./deploy_synology.sh --env-file .env.deploy_synology --git-hours 2 ./deploy_synology.sh --git-hours 2 --sudo-mode always ./deploy_synology.sh --files "web/src/web_core/ui/help_content.py" EOF } run_cmd() { if [[ "$DRY_RUN" == "1" ]]; then printf '[dry-run] %s\n' "$*" else eval "$@" fi } build_ssh_cmd() { if [[ -n "${SYNO_PASSWORD:-}" ]]; then if ! command -v sshpass >/dev/null 2>&1; then if [[ "$SSH_PASS_WARNING_SHOWN" == "0" ]]; then echo "Warning: SYNO_PASSWORD is set but sshpass is not installed; falling back to interactive ssh/scp prompts." >&2 SSH_PASS_WARNING_SHOWN="1" fi printf "ssh -p '%s' '%s@%s'" "${REMOTE_PORT}" "${REMOTE_USER}" "${REMOTE_HOST}" return fi printf "SSHPASS='%s' sshpass -e ssh -o StrictHostKeyChecking=accept-new -p '%s' '%s@%s'" \ "${SYNO_PASSWORD}" "${REMOTE_PORT}" "${REMOTE_USER}" "${REMOTE_HOST}" else printf "ssh -p '%s' '%s@%s'" "${REMOTE_PORT}" "${REMOTE_USER}" "${REMOTE_HOST}" fi } build_scp_cmd() { if [[ -n "${SYNO_PASSWORD:-}" ]]; then if ! command -v sshpass >/dev/null 2>&1; then if [[ "$SSH_PASS_WARNING_SHOWN" == "0" ]]; then echo "Warning: SYNO_PASSWORD is set but sshpass is not installed; falling back to interactive ssh/scp prompts." >&2 SSH_PASS_WARNING_SHOWN="1" fi printf "scp -O -P '%s'" "${REMOTE_PORT}" return fi printf "SSHPASS='%s' sshpass -e scp -O -o StrictHostKeyChecking=accept-new -P '%s'" \ "${SYNO_PASSWORD}" "${REMOTE_PORT}" else printf "scp -O -P '%s'" "${REMOTE_PORT}" fi } to_remote_rel_path() { local file="$1" local prefix="${LOCAL_BASE%/}/" if [[ "$file" == "$prefix"* ]]; then printf "%s" "${file#$prefix}" else printf "%s" "$file" fi } collect_git_hours_files() { local hours="$1" local base_commit local combined if ! command -v git >/dev/null 2>&1; then echo "Error: git is required for --git-hours." >&2 exit 1 fi if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo "Error: --git-hours must be run from inside a git repository." >&2 exit 1 fi base_commit="$(git rev-list -n 1 --before="${hours} hours ago" HEAD 2>/dev/null || true)" combined="$( { if [[ -n "$base_commit" ]]; then git diff --name-only --diff-filter=ACMRTUXB "${base_commit}..HEAD" -- web/src fi git diff --name-only -- web/src git diff --cached --name-only -- web/src git ls-files -m -o --exclude-standard -- web/src } | sed '/^$/d' | sort -u )" FILES=() while IFS= read -r candidate; do [[ -z "$candidate" ]] && continue FILES+=("$candidate") done <&2 exit 1 fi # shellcheck disable=SC1090 set -a; source "$ENV_FILE"; set +a REMOTE_USER="${REMOTE_USER:-mbrucedogs}" REMOTE_HOST="${REMOTE_HOST:-192.168.1.128}" REMOTE_PORT="${REMOTE_PORT:-25}" REMOTE_BASE="${REMOTE_BASE:-/volume1/docker/maneshtrader}" LOCAL_BASE="${LOCAL_BASE:-web/src}" SYNO_PASSWORD="${SYNO_PASSWORD:-}" CONTAINER_NAME="${CONTAINER_NAME:-}" SUDO_PASSWORD="${SUDO_PASSWORD:-${SYNO_PASSWORD:-}}" SUDO_MODE="${SUDO_MODE:-auto}" shift 2 ;; --files) read -r -a FILES <<< "${2:-}" shift 2 ;; --recent-minutes) minutes="${2:-}" if ! [[ "$minutes" =~ ^[0-9]+$ ]]; then echo "Error: --recent-minutes requires an integer." >&2 exit 1 fi FILES=() while IFS= read -r candidate; do [[ -z "$candidate" ]] && continue FILES+=("$candidate") done <&2 exit 1 fi collect_git_hours_files "$hours" shift 2 ;; --no-restart) NO_RESTART="1" shift ;; --sudo-mode) SUDO_MODE="${2:-}" shift 2 ;; --dry-run) DRY_RUN="1" shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown option: $1" >&2 usage exit 1 ;; esac done if [[ "$MODE" != "bind" && "$MODE" != "image" ]]; then echo "Error: --mode must be 'bind' or 'image'." >&2 exit 1 fi if [[ "$SUDO_MODE" != "auto" && "$SUDO_MODE" != "always" && "$SUDO_MODE" != "never" ]]; then echo "Error: --sudo-mode must be auto|always|never." >&2 exit 1 fi if [[ ${#FILES[@]} -eq 0 ]]; then echo "Error: No files selected for upload." >&2 exit 1 fi for file in "${FILES[@]}"; do if [[ ! -f "$file" ]]; then echo "Error: Local file not found: $file" >&2 exit 1 fi done echo "Deploy target: ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT}" echo "Remote base: ${REMOTE_BASE}" echo "Mode: ${MODE}" echo "Files:" for file in "${FILES[@]}"; do echo " - $file" done for file in "${FILES[@]}"; do remote_rel="$(to_remote_rel_path "$file")" remote_path="${REMOTE_BASE}/${remote_rel}" remote_dir="$(dirname "$remote_path")" SSH_CMD="$(build_ssh_cmd)" SCP_CMD="$(build_scp_cmd)" run_cmd "${SSH_CMD} \"mkdir -p '${remote_dir}'\"" run_cmd "${SCP_CMD} '${file}' '${REMOTE_USER}@${REMOTE_HOST}:${remote_path}'" done if [[ "$NO_RESTART" == "1" ]]; then echo "Upload complete (restart/rebuild skipped)." exit 0 fi if [[ "$MODE" == "bind" ]]; then SSH_CMD="$(build_ssh_cmd)" run_cmd "${SSH_CMD} ' set -e cd \"${REMOTE_BASE}\" SUDO_MODE=\"${SUDO_MODE}\" SUDO_PASSWORD=\"${SUDO_PASSWORD}\" DOCKER_BIN=\"\" if command -v docker >/dev/null 2>&1; then DOCKER_BIN=\"\$(command -v docker)\" elif [[ -x /usr/local/bin/docker ]]; then DOCKER_BIN=\"/usr/local/bin/docker\" elif [[ -x /usr/bin/docker ]]; then DOCKER_BIN=\"/usr/bin/docker\" elif [[ -x /var/packages/ContainerManager/target/usr/bin/docker ]]; then DOCKER_BIN=\"/var/packages/ContainerManager/target/usr/bin/docker\" fi docker_cmd() { if [[ \"\$USE_SUDO\" == \"1\" ]]; then if [[ -n \"\$SUDO_PASSWORD\" ]]; then printf \"%s\\n\" \"\$SUDO_PASSWORD\" | sudo -S -p \"\" \"\$DOCKER_BIN\" \"\$@\" else sudo \"\$DOCKER_BIN\" \"\$@\" fi else \"\$DOCKER_BIN\" \"\$@\" fi } USE_SUDO=\"0\" if [[ \"\$SUDO_MODE\" == \"always\" ]]; then USE_SUDO=\"1\" elif [[ \"\$SUDO_MODE\" == \"auto\" ]]; then if ! \"\$DOCKER_BIN\" info >/dev/null 2>&1; then USE_SUDO=\"1\" fi fi if [[ -n \"\$DOCKER_BIN\" ]] && docker_cmd compose version >/dev/null 2>&1; then docker_cmd compose restart elif command -v docker-compose >/dev/null 2>&1; then docker-compose restart elif [[ -n \"\$DOCKER_BIN\" ]]; then if [[ -n \"${CONTAINER_NAME:-}\" ]]; then docker_cmd restart \"${CONTAINER_NAME}\" else ids=\$(docker_cmd ps --filter \"name=maneshtrader\" -q) if [[ -z \"\$ids\" ]]; then echo \"No docker compose command found, and no running containers matched name=maneshtrader.\" >&2 echo \"Set CONTAINER_NAME= and rerun.\" >&2 exit 1 fi docker_cmd restart \$ids fi else echo \"No docker or docker compose command found on remote host.\" >&2 exit 1 fi '" echo "Upload + container restart complete." else SSH_CMD="$(build_ssh_cmd)" run_cmd "${SSH_CMD} ' set -e cd \"${REMOTE_BASE}\" SUDO_MODE=\"${SUDO_MODE}\" SUDO_PASSWORD=\"${SUDO_PASSWORD}\" DOCKER_BIN=\"\" if command -v docker >/dev/null 2>&1; then DOCKER_BIN=\"\$(command -v docker)\" elif [[ -x /usr/local/bin/docker ]]; then DOCKER_BIN=\"/usr/local/bin/docker\" elif [[ -x /usr/bin/docker ]]; then DOCKER_BIN=\"/usr/bin/docker\" elif [[ -x /var/packages/ContainerManager/target/usr/bin/docker ]]; then DOCKER_BIN=\"/var/packages/ContainerManager/target/usr/bin/docker\" fi docker_cmd() { if [[ \"\$USE_SUDO\" == \"1\" ]]; then if [[ -n \"\$SUDO_PASSWORD\" ]]; then printf \"%s\\n\" \"\$SUDO_PASSWORD\" | sudo -S -p \"\" \"\$DOCKER_BIN\" \"\$@\" else sudo \"\$DOCKER_BIN\" \"\$@\" fi else \"\$DOCKER_BIN\" \"\$@\" fi } USE_SUDO=\"0\" if [[ \"\$SUDO_MODE\" == \"always\" ]]; then USE_SUDO=\"1\" elif [[ \"\$SUDO_MODE\" == \"auto\" ]]; then if ! \"\$DOCKER_BIN\" info >/dev/null 2>&1; then USE_SUDO=\"1\" fi fi if [[ -n \"\$DOCKER_BIN\" ]] && docker_cmd compose version >/dev/null 2>&1; then docker_cmd compose up -d --build elif command -v docker-compose >/dev/null 2>&1; then docker-compose up -d --build else echo \"No docker compose command found on remote host.\" >&2 exit 1 fi '" echo "Upload + image rebuild complete." fi