Raiju/deploy.example.sh
2026-10-25 06:05:42 +03:00

212 lines
7.6 KiB
Bash
Executable File

#!/bin/bash
# ============================================================
# Raiju CMS — FTPS Deployment Script (three-phase sync)
# ============================================================
#
# Synchronises a Raiju site between your local working copy and a
# remote FTPS server using lftp. Designed to preserve data the admin
# writes server-side (inline-editor changes, image uploads, contact
# form submissions) while still treating local code as the source of
# truth for templates, JS, and CSS.
#
# WHY THREE PHASES?
# Raiju is a flat-file CMS. Pages live as JSON in content/, images
# under assets/images/. Both are written by admins on the LIVE
# server. A naive `mirror --reverse --delete` would overwrite those
# edits with whatever older copies sit in the local repo, and would
# delete server-uploaded images that aren't in the local checkout.
# This script avoids that by:
# Phase 1 — PULL newer admin-written files from the server first.
# Phase 2 — PUSH local code (excluding admin-written paths) with
# --delete so removed files actually disappear.
# Phase 3 — PUSH local admin-written files only if newer (no
# --delete), so a deliberate local edit can still ship
# but no server file is silently dropped.
#
# AFTER THE FIRST UPLOAD — MAKE SURE INTERNAL FOLDERS ARE BLOCKED:
# .htaccess protects content/, backups/, cli/, tests/ and the media
# trash folders only for requests that reach Apache. If nginx serves
# static files itself (HestiaCP; Plesk with "Serve static files
# directly by nginx"), password hashes, the SMTP password, form
# submissions and backup ZIPs become public. Add to the site's nginx
# configuration (prefix the paths for a subdirectory installation):
#
# location ^~ /content/ { deny all; }
# location ^~ /backups/ { deny all; }
# location ^~ /cli/ { deny all; }
# location ^~ /tests/ { deny all; }
# location ^~ /assets/images-trash/ { deny all; }
# location ^~ /assets/audio-trash/ { deny all; }
# location ^~ /assets/videos-trash/ { deny all; }
# location ^~ /assets/documents-trash/ { deny all; }
#
# HestiaCP: save as /home/<user>/conf/web/<domain>/nginx.ssl.conf_raiju
# (plain HTTP: also nginx.conf_raiju), then as root:
# nginx -t && systemctl reload nginx
# Plesk: keep "Serve static files directly by nginx" switched off,
# or add the lines under "Additional nginx directives".
# Then open Dashboard -> System status -> Security and click "Check
# now". By hand, test an EXISTING file (nginx passes missing files to
# Apache, which answers 403 anyway):
# curl -I https://example.com/content/settings.json # expect 403/404
# Details: ROUTING.md, "Hosting proxies and internal folders".
#
# PREREQUISITES:
# - lftp must be installed:
# macOS: brew install lftp
# Linux: sudo apt-get install lftp
#
# SETUP:
# 1. Copy this file to deploy.sh:
# cp deploy.example.sh deploy.sh
# 2. Edit deploy.sh and fill in your FTP credentials below.
# 3. deploy.sh is already in .gitignore — your credentials will
# never be committed.
# 4. Run: bash deploy.sh
#
# IMPORTANT — lftp script file approach:
# This script writes lftp commands to a temporary file and runs
# them with `lftp -f`. This is intentional. Passing lftp commands
# via heredoc (<< EOF) or inline (-e "...") causes backslash line
# continuations and long option flags (like --verbose, --newer,
# --no-perms) to be misinterpreted — flags get treated as file
# paths, resulting in "No such file or directory" errors and
# silent misbehavior (e.g. --newer being ignored). The temp file
# approach avoids all shell quoting/escaping issues.
#
# ============================================================
set -o pipefail
# --- Server configuration (edit these) -----------------------
SERVER="YOUR_SERVER" # e.g. example.com or ftp.example.com
USER="YOUR_USER" # FTP username
PASS="YOUR_PASSWORD" # FTP password
REMOTE_DIR="/httpdocs/" # Remote directory (document root)
# -------------------------------------------------------------
# Local directory (directory where this script lives)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
echo -e "${YELLOW}=== Deploying Raiju (three-phase sync) ===${NC}"
echo "Local: $SCRIPT_DIR"
echo "Remote: $SERVER:$REMOTE_DIR"
echo ""
# Check if lftp is installed
if ! command -v lftp &> /dev/null; then
echo -e "${RED}lftp is not installed.${NC}"
echo "Install it first:"
echo " macOS: brew install lftp"
echo " Linux: sudo apt-get install lftp"
exit 1
fi
cd "$SCRIPT_DIR" || exit 1
# OS-junk and editor-junk excludes — applied in EVERY mirror call.
# (lftp's mirror:exclude-regex setting is unreliable in 4.9.x; keeping
# these as per-call --exclude-glob is the only robust approach.)
OS_EXCLUDES="\
--exclude-glob .DS_Store \
--exclude-glob ._* \
--exclude-glob Thumbs.db \
--exclude-glob *.tmp \
--exclude-glob *.swp"
# Code excludes — never push or pull these.
CODE_EXCLUDES="\
--exclude-glob .git/ \
--exclude-glob node_modules/ \
--exclude-glob .gitignore \
--exclude-glob .gitattributes \
--exclude-glob deploy.sh \
--exclude-glob deploy.example.sh \
--exclude-glob screenshots/ \
--exclude-glob reference/ \
--exclude-glob not-for-git/ \
--exclude-glob *.mjs \
--exclude-glob *.log \
--exclude-glob package.json \
--exclude-glob package-lock.json"
# Paths the admin writes server-side. Excluded from the code-push
# phase, handled separately in the content-push phase with --only-newer.
ADMIN_WRITTEN_EXCLUDES="\
--exclude-glob content/ \
--exclude-glob assets/images/ \
--exclude-glob backups/"
# Write lftp commands to a temp file (see IMPORTANT note above)
LFTP_SCRIPT=$(mktemp)
cat > "$LFTP_SCRIPT" << LFTP
set ftp:ssl-allow yes
set ftp:ssl-force yes
set ftp:ssl-protect-data yes
set ftp:passive-mode yes
set ssl:verify-certificate no
set mirror:use-pget-n 5
set net:max-retries 2
set net:timeout 20
open -u $USER,"$PASS" $SERVER
mkdir -p $REMOTE_DIR
cd $REMOTE_DIR
# Phase 1: PULL — newer server-side admin writes back to local.
# No --delete — we never let the server tell us to delete local files.
echo ""
echo "[Phase 1/3] Pull newer admin-written files from server..."
mirror --only-newer -v --no-perms \
$OS_EXCLUDES \
content/ $SCRIPT_DIR/content/
mirror --only-newer -v --no-perms \
$OS_EXCLUDES \
assets/images/ $SCRIPT_DIR/assets/images/
# Phase 2: PUSH — local code is the source of truth. Use --delete so
# removed templates/CSS/JS actually disappear from the server.
# Excludes admin-written paths — those are handled in Phase 3.
echo ""
echo "[Phase 2/3] Push code (templates, CSS, JS, includes/) with --delete..."
mirror --reverse --delete -v --no-perms \
$OS_EXCLUDES \
$CODE_EXCLUDES \
$ADMIN_WRITTEN_EXCLUDES \
. .
# Phase 3: PUSH — admin-written paths, only if local copy is newer.
# No --delete — never silently drop a server-side file just because
# it doesn't exist locally (e.g. an admin-uploaded image).
echo ""
echo "[Phase 3/3] Push admin-written paths only if local is newer..."
mirror --reverse --only-newer -v --no-perms \
$OS_EXCLUDES \
$SCRIPT_DIR/content/ content/
mirror --reverse --only-newer -v --no-perms \
$OS_EXCLUDES \
$SCRIPT_DIR/assets/images/ assets/images/
bye
LFTP
lftp -f "$LFTP_SCRIPT"
RESULT=$?
rm -f "$LFTP_SCRIPT"
if [ $RESULT -eq 0 ]; then
echo ""
echo -e "${GREEN}=== Deploy complete ===${NC}"
else
echo ""
echo -e "${RED}=== Deploy failed ===${NC}"
exit 1
fi