#!/bin/sh # SecuBox Hexo CMS Controller # Copyright (C) 2025 CyberMind.fr # # Manages Hexo static site generator in LXC container # Supports multiple instances on different ports CONFIG="hexojs" LXC_NAME="hexojs" # Paths LXC_PATH="/srv/lxc" LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs" LXC_CONFIG="$LXC_PATH/$LXC_NAME/config" SHARE_PATH="/usr/share/hexojs" # Logging log_info() { echo "[INFO] $*"; logger -t hexojs "$*"; } log_warn() { echo "[WARN] $*" >&2; logger -t hexojs -p warning "$*"; } log_error() { echo "[ERROR] $*" >&2; logger -t hexojs -p err "$*"; } log_debug() { [ "$DEBUG" = "1" ] && echo "[DEBUG] $*"; } # Helpers require_root() { [ "$(id -u)" -eq 0 ] || { log_error "This command requires root privileges" exit 1 } } has_lxc() { command -v lxc-start >/dev/null 2>&1; } has_git() { command -v git >/dev/null 2>&1; } ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; } uci_get() { uci -q get ${CONFIG}.$1; } uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; } # Load main configuration load_config() { data_path="$(uci_get main.data_path)" || data_path="/srv/hexojs" memory_limit="$(uci_get main.memory_limit)" || memory_limit="512M" # Legacy support: active_site for default instance active_site="$(uci_get main.active_site)" || active_site="default" http_port="$(uci_get main.http_port)" || http_port="4000" # Gitea config (shared) gitea_enabled="$(uci_get gitea.enabled)" || gitea_enabled="0" gitea_url="$(uci_get gitea.url)" || gitea_url="http://192.168.255.1:3000" gitea_user="$(uci_get gitea.user)" || gitea_user="admin" gitea_token="$(uci_get gitea.token)" || gitea_token="" gitea_content_repo="$(uci_get gitea.content_repo)" || gitea_content_repo="blog-content" gitea_content_branch="$(uci_get gitea.content_branch)" || gitea_content_branch="main" ensure_dir "$data_path" ensure_dir "$data_path/instances" ensure_dir "$data_path/themes" } # Load instance configuration load_instance_config() { local instance="$1" [ -z "$instance" ] && instance="default" current_instance="$instance" # Check if instance section exists local instance_type=$(uci_get "${instance}") if [ "$instance_type" != "instance" ]; then # Legacy: check if it's old-style site section or doesn't exist if [ -z "$instance_type" ] && [ "$instance" = "default" ]; then # Use legacy main config for default instance_port="$http_port" instance_title="$(uci_get default.title)" || instance_title="My Blog" instance_theme="$(uci_get default.theme)" || instance_theme="cybermind" instance_enabled="1" else instance_port="" instance_title="" instance_theme="" instance_enabled="0" return 1 fi else instance_port="$(uci_get ${instance}.port)" || instance_port="4000" instance_title="$(uci_get ${instance}.title)" || instance_title="My Blog" instance_theme="$(uci_get ${instance}.theme)" || instance_theme="cybermind" instance_enabled="$(uci_get ${instance}.enabled)" || instance_enabled="0" fi instance_path="$data_path/instances/$instance" instance_site="$instance_path/site" return 0 } # Get list of all enabled instances get_enabled_instances() { local instances="" # Check for instance sections in UCI for section in $(uci show hexojs 2>/dev/null | grep '=instance$' | cut -d'.' -f2 | cut -d'=' -f1); do local enabled=$(uci_get "${section}.enabled") [ "$enabled" = "1" ] && instances="$instances $section" done # If no instances defined, check for legacy default if [ -z "$instances" ]; then if [ -d "$data_path/site" ] || [ -d "$data_path/instances/default/site" ]; then instances="default" fi fi echo "$instances" } # Usage usage() { cat < [options] Container Commands: install Download and setup Hexo LXC container uninstall Remove Hexo container (keeps data) update Update Hexo and dependencies status Show service status Instance Management: instance list List all instances instance create Create new instance instance delete Delete an instance instance start Start instance server instance stop Stop instance server instance status Show instance status Site Management (operates on current/specified instance): site create [instance] Create Hexo site for instance site delete [instance] Delete site for instance Content Commands: new post "Title" [instance] Create new blog post new page "Title" [instance] Create new page new draft "Title" [instance] Create new draft list posts [instance] List all posts list drafts [instance] List all drafts Build Commands: serve [instance] Start preview server build [instance] Generate static files clean [instance] Clean generated files deploy [instance] Deploy to configured target publish [instance] Copy static files to /www/ Service Commands: service-run Run all instances (for init) service-stop Stop all instances Gitea Integration: gitea setup [instance] Configure git credentials gitea clone [instance] Clone content repo gitea sync [instance] Pull latest content gitea push [instance] Push changes to Gitea GitHub Integration: github clone [instance] [branch] Clone from GitHub Backup/Restore: backup [instance] [name] Create backup backup list List all backups backup delete Delete a backup restore [instance] Restore from backup Quick Commands: quick-publish [instance] Clean, build, and publish User Management: user add [password] Add user with password user del Delete user user list List all users user passwd [pass] Change user password user grant Grant access to instance user revoke Revoke access to instance user sync-secubox Import users from SecuBox/LuCI Authentication: auth enable Enable auth for instance auth disable Disable auth for instance auth status [instance] Show auth status auth haproxy Generate HAProxy auth ACLs auth apply [domain] Auto-configure HAProxy with auth KISS Static Sites (No Hexo Build): static create Create static-only site static upload [inst] Upload HTML/CSS/JS directly static publish [instance] Publish to /www/ for immediate serving static quick [inst] Upload + publish in one step static list [instance] List static files static serve [instance] Serve static files (Python/httpd) static delete Delete static instance Utility: shell Open shell in container logs [instance] View logs exec Execute command in container Examples: hexoctl instance create myblog # Create new instance hexoctl instance start myblog # Start on configured port hexoctl site create myblog # Initialize Hexo site hexoctl new post "Hello" myblog # Create post in myblog Configuration: /etc/config/hexojs EOF } # Check prerequisites lxc_check_prereqs() { if ! has_lxc; then log_error "LXC not installed. Install with: opkg install lxc lxc-common" return 1 fi return 0 } # Create Node.js LXC rootfs from Alpine lxc_create_rootfs() { local rootfs="$LXC_ROOTFS" local arch=$(uname -m) log_info "Creating Alpine rootfs for Hexo..." ensure_dir "$rootfs" local alpine_version="3.21" case "$arch" in x86_64) alpine_arch="x86_64" ;; aarch64) alpine_arch="aarch64" ;; armv7l) alpine_arch="armv7" ;; *) log_error "Unsupported architecture: $arch"; return 1 ;; esac local alpine_url="https://dl-cdn.alpinelinux.org/alpine/v${alpine_version}/releases/${alpine_arch}/alpine-minirootfs-${alpine_version}.0-${alpine_arch}.tar.gz" local tmpfile="/tmp/alpine-rootfs.tar.gz" log_info "Downloading Alpine rootfs..." wget -q -O "$tmpfile" "$alpine_url" || { log_error "Failed to download Alpine rootfs" return 1 } log_info "Extracting rootfs..." tar -xzf "$tmpfile" -C "$rootfs" || { log_error "Failed to extract rootfs" return 1 } rm -f "$tmpfile" # Setup resolv.conf cp /etc/resolv.conf "$rootfs/etc/resolv.conf" 2>/dev/null || \ echo "nameserver 1.1.1.1" > "$rootfs/etc/resolv.conf" # Create Hexo directory ensure_dir "$rootfs/opt/hexojs" log_info "Rootfs created successfully" return 0 } # Create LXC config lxc_create_config() { load_config ensure_dir "$(dirname "$LXC_CONFIG")" # Convert memory limit to bytes local mem_bytes case "$memory_limit" in *G|*g) mem_bytes=$((${memory_limit%[Gg]} * 1024 * 1024 * 1024)) ;; *M|*m) mem_bytes=$((${memory_limit%[Mm]} * 1024 * 1024)) ;; *K|*k) mem_bytes=$((${memory_limit%[Kk]} * 1024)) ;; *) mem_bytes="$memory_limit" ;; esac cat > "$LXC_CONFIG" << EOF # Hexo CMS LXC Configuration (Multi-Instance) lxc.uts.name = $LXC_NAME lxc.rootfs.path = dir:$LXC_ROOTFS lxc.arch = $(uname -m) # Network: use host network lxc.net.0.type = none # Mount points lxc.mount.auto = proc:mixed sys:ro cgroup:mixed lxc.mount.entry = $data_path opt/hexojs none bind,create=dir 0 0 # Environment lxc.environment = NODE_ENV=production # Security lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time sys_rawio # Resource limits lxc.cgroup.memory.limit_in_bytes = $mem_bytes # Init command - multi-instance manager lxc.init.cmd = /opt/start-hexo-multi.sh EOF log_info "LXC config created" } # Create multi-instance startup script create_startup_script() { load_config local start_script="$LXC_ROOTFS/opt/start-hexo-multi.sh" cat > "$start_script" << 'STARTEOF' #!/bin/sh export PATH=/usr/local/bin:/usr/bin:/bin:$PATH export HOME=/root export NODE_ENV=production HEXO_BASE="/opt/hexojs" INSTANCES_DIR="$HEXO_BASE/instances" PIDS_DIR="/var/run/hexo" LOG_DIR="/var/log/hexo" mkdir -p "$PIDS_DIR" "$LOG_DIR" # Install dependencies on first run if [ ! -f /opt/.installed ]; then echo "Installing Node.js and Hexo..." apk update apk add --no-cache nodejs npm git openssh-client npm install -g hexo-cli touch /opt/.installed fi # Function to start a single instance start_instance() { local name="$1" local port="$2" local site_dir="$INSTANCES_DIR/$name/site" [ -d "$site_dir" ] || return 1 [ -f "$site_dir/package.json" ] || return 1 echo "Starting instance '$name' on port $port..." cd "$site_dir" [ -d "node_modules" ] || npm install # Start hexo server in background nohup npx hexo server -p "$port" -i 0.0.0.0 > "$LOG_DIR/$name.log" 2>&1 & echo $! > "$PIDS_DIR/$name.pid" echo "Instance '$name' started (PID: $!)" } # Function to stop an instance stop_instance() { local name="$1" local pidfile="$PIDS_DIR/$name.pid" if [ -f "$pidfile" ]; then local pid=$(cat "$pidfile") if kill -0 "$pid" 2>/dev/null; then kill "$pid" echo "Stopped instance '$name' (PID: $pid)" fi rm -f "$pidfile" fi } # Read instances config from file INSTANCES_CONF="$HEXO_BASE/instances.conf" # Main loop - keep container running if [ -f "$INSTANCES_CONF" ]; then echo "Loading instances from config..." while IFS=: read -r name port; do [ -n "$name" ] && [ -n "$port" ] && start_instance "$name" "$port" done < "$INSTANCES_CONF" fi # Legacy: check for old-style single site if [ -d "$HEXO_BASE/site" ] && [ ! -L "$HEXO_BASE/site" ]; then echo "Starting legacy site on port ${HEXO_PORT:-4000}..." cd "$HEXO_BASE/site" [ -d "node_modules" ] || npm install nohup npx hexo server -p "${HEXO_PORT:-4000}" -i 0.0.0.0 > "$LOG_DIR/default.log" 2>&1 & echo $! > "$PIDS_DIR/default.pid" fi # Keep container running echo "Hexo multi-instance manager running. Instances:" ls -1 "$PIDS_DIR"/*.pid 2>/dev/null | while read f; do name=$(basename "$f" .pid) pid=$(cat "$f") echo " - $name (PID: $pid)" done # Wait forever exec tail -f /dev/null STARTEOF chmod +x "$start_script" } # Generate instances.conf for container generate_instances_conf() { load_config local conf_file="$data_path/instances.conf" > "$conf_file" for instance in $(get_enabled_instances); do load_instance_config "$instance" || continue [ "$instance_enabled" = "1" ] || continue [ -d "$instance_site" ] || continue echo "${instance}:${instance_port}" >> "$conf_file" done log_debug "Generated instances.conf with $(wc -l < "$conf_file") instances" } # Container control lxc_running() { lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING" } lxc_exists() { [ -f "$LXC_CONFIG" ] && [ -d "$LXC_ROOTFS" ] } lxc_stop() { if lxc_running; then log_info "Stopping Hexo container..." lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true sleep 2 fi } lxc_run() { load_config lxc_stop if ! lxc_exists; then log_error "Container not installed. Run: hexoctl install" return 1 fi # Regenerate config lxc_create_config create_startup_script generate_instances_conf log_info "Starting Hexo container..." exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG" } lxc_exec() { if ! lxc_running; then log_error "Container not running. Start with: /etc/init.d/hexojs start" return 1 fi lxc-attach -n "$LXC_NAME" -- env PATH=/usr/local/bin:/usr/bin:/bin "$@" } # Instance management commands cmd_instance_list() { load_config echo "Hexo Instances:" echo "---------------" local found=0 for section in $(uci show hexojs 2>/dev/null | grep '=instance$' | cut -d'.' -f2 | cut -d'=' -f1); do found=1 load_instance_config "$section" local status="disabled" [ "$instance_enabled" = "1" ] && status="enabled" local site_status="no site" [ -d "$instance_site" ] && site_status="site ready" local running="" if lxc_running && [ -f "$LXC_ROOTFS/var/run/hexo/${section}.pid" ]; then running=" [RUNNING]" fi printf " %-15s port:%-5s %s (%s)%s\n" "$section" "$instance_port" "[$status]" "$site_status" "$running" done # Check for legacy default if [ "$found" = "0" ] && [ -d "$data_path/site" ]; then echo " default port:$http_port [legacy] (site ready)" fi [ "$found" = "0" ] && [ ! -d "$data_path/site" ] && echo " (no instances)" } cmd_instance_create() { require_root load_config local name="$1" [ -z "$name" ] && { log_error "Instance name required"; return 1; } # Validate name echo "$name" | grep -qE '^[a-z][a-z0-9_]*$' || { log_error "Invalid instance name. Use lowercase letters, numbers, underscore. Start with letter." return 1 } # Check if exists local existing=$(uci_get "$name") [ -n "$existing" ] && { log_error "Instance '$name' already exists"; return 1; } # Find next available port local port=4000 while uci show hexojs 2>/dev/null | grep -q "port='$port'"; do port=$((port + 1)) done # Create UCI config uci set hexojs.${name}=instance uci set hexojs.${name}.enabled='1' uci set hexojs.${name}.port="$port" uci set hexojs.${name}.title="$name Blog" uci set hexojs.${name}.theme='cybermind' uci commit hexojs # Create directory ensure_dir "$data_path/instances/$name" log_info "Instance '$name' created on port $port" log_info "Next: hexoctl site create $name" } cmd_instance_delete() { require_root load_config local name="$1" [ -z "$name" ] && { log_error "Instance name required"; return 1; } # Stop instance first cmd_instance_stop "$name" 2>/dev/null # Remove UCI config uci delete hexojs.${name} 2>/dev/null uci commit hexojs # Optionally remove data (ask user) local instance_dir="$data_path/instances/$name" if [ -d "$instance_dir" ]; then log_warn "Data directory exists: $instance_dir" log_info "Remove manually if needed: rm -rf $instance_dir" fi log_info "Instance '$name' deleted" } cmd_instance_start() { require_root load_config local name="$1" [ -z "$name" ] && { log_error "Instance name required"; return 1; } load_instance_config "$name" || { log_error "Instance '$name' not found"; return 1; } if ! lxc_running; then log_error "Container not running. Start with: /etc/init.d/hexojs start" return 1 fi if [ ! -d "$instance_site" ]; then log_error "No site for instance '$name'. Create with: hexoctl site create $name" return 1 fi log_info "Starting instance '$name' on port $instance_port..." lxc_exec sh -c " cd /opt/hexojs/instances/$name/site || exit 1 [ -d node_modules ] || npm install # Kill existing if running [ -f /var/run/hexo/$name.pid ] && kill \$(cat /var/run/hexo/$name.pid) 2>/dev/null mkdir -p /var/run/hexo /var/log/hexo nohup npx hexo server -p $instance_port -i 0.0.0.0 > /var/log/hexo/$name.log 2>&1 & echo \$! > /var/run/hexo/$name.pid echo \"Started on port $instance_port (PID: \$!)\" " # Update instances.conf generate_instances_conf } cmd_instance_stop() { require_root load_config local name="$1" [ -z "$name" ] && { log_error "Instance name required"; return 1; } if ! lxc_running; then return 0 fi log_info "Stopping instance '$name'..." lxc_exec sh -c " if [ -f /var/run/hexo/$name.pid ]; then kill \$(cat /var/run/hexo/$name.pid) 2>/dev/null rm -f /var/run/hexo/$name.pid echo 'Stopped' else echo 'Not running' fi " } cmd_instance_status() { load_config local name="$1" [ -z "$name" ] && { log_error "Instance name required"; return 1; } load_instance_config "$name" || { log_error "Instance '$name' not found"; return 1; } local running="false" local pid="" if lxc_running; then pid=$(lxc_exec cat /var/run/hexo/$name.pid 2>/dev/null) if [ -n "$pid" ] && lxc_exec kill -0 "$pid" 2>/dev/null; then running="true" fi fi local site_exists="false" [ -d "$instance_site" ] && site_exists="true" cat << EOF Instance: $name -------------- Enabled: $([ "$instance_enabled" = "1" ] && echo "yes" || echo "no") Running: $([ "$running" = "true" ] && echo "yes (PID: $pid)" || echo "no") Port: $instance_port Title: $instance_title Theme: $instance_theme Site: $([ "$site_exists" = "true" ] && echo "ready" || echo "not created") Path: $instance_path EOF if [ "$running" = "true" ]; then echo "URL: http://$(uci -q get network.lan.ipaddr || echo 'localhost'):$instance_port" fi } # Commands cmd_install() { require_root load_config log_info "Installing Hexo CMS..." lxc_check_prereqs || exit 1 if ! lxc_exists; then lxc_create_rootfs || exit 1 fi lxc_create_config || exit 1 create_startup_script # Copy theme if [ -d "$SHARE_PATH/themes/cybermind" ]; then log_info "Installing CyberMind theme..." ensure_dir "$data_path/themes" cp -r "$SHARE_PATH/themes/cybermind" "$data_path/themes/" fi # Copy scaffolds if [ -d "$SHARE_PATH/scaffolds" ]; then ensure_dir "$data_path/scaffolds" cp -r "$SHARE_PATH/scaffolds/"* "$data_path/scaffolds/" fi log_info "Installation complete!" log_info "" log_info "Next steps:" log_info " 1. Create instance: hexoctl instance create myblog" log_info " 2. Create site: hexoctl site create myblog" log_info " 3. Start service: /etc/init.d/hexojs start" } cmd_uninstall() { require_root log_info "Uninstalling Hexo CMS..." /etc/init.d/hexojs stop 2>/dev/null || true /etc/init.d/hexojs disable 2>/dev/null || true lxc_stop if [ -d "$LXC_PATH/$LXC_NAME" ]; then rm -rf "$LXC_PATH/$LXC_NAME" log_info "Container removed" fi log_info "Hexo CMS uninstalled" log_info "Data preserved in: $(uci_get main.data_path)" } cmd_update() { require_root load_config log_info "Updating Hexo CMS..." if ! lxc_running; then log_error "Container not running" return 1 fi lxc_exec sh -c 'npm update -g hexo-cli' # Update each instance for instance in $(get_enabled_instances); do load_instance_config "$instance" || continue if [ -d "$instance_site" ]; then log_info "Updating instance '$instance'..." lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && npm update" fi done log_info "Update complete!" } cmd_status() { load_config local enabled="$(uci_get main.enabled)" local running="false" lxc_running && running="true" cat << EOF Hexo CMS Status =============== Enabled: $([ "$enabled" = "1" ] && echo "yes" || echo "no") Running: $([ "$running" = "true" ] && echo "yes" || echo "no") Data Path: $data_path Memory: $memory_limit Container: $LXC_NAME Instances: EOF for instance in $(get_enabled_instances); do load_instance_config "$instance" || continue local status="stopped" if [ "$running" = "true" ]; then local pid=$(lxc_exec cat /var/run/hexo/$instance.pid 2>/dev/null) [ -n "$pid" ] && status="running:$instance_port" fi printf " %-15s %s\n" "$instance" "[$status]" done } # Site management (instance-aware) cmd_site_create() { require_root load_config local instance="${1:-default}" load_instance_config "$instance" || { # Auto-create instance if it doesn't exist log_info "Creating instance '$instance'..." cmd_instance_create "$instance" load_instance_config "$instance" } log_info "Creating Hexo site for instance: $instance" if [ -d "$instance_site" ]; then log_error "Site already exists at $instance_site" return 1 fi ensure_dir "$instance_path" # Start container if not running local was_stopped=0 if ! lxc_running; then was_stopped=1 lxc_create_config create_startup_script lxc-start -n "$LXC_NAME" -d -f "$LXC_CONFIG" sleep 5 fi # Create site in container lxc_exec sh -c "cd /opt/hexojs/instances/$instance && hexo init site" || { log_error "Failed to initialize site" return 1 } # Install dependencies lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && npm install" || { log_error "Failed to install dependencies" return 1 } # Install deploy plugin lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && npm install hexo-deployer-git --save" || true # Install theme if [ -d "$data_path/themes/cybermind" ]; then log_info "Installing CyberMind theme..." cp -r "$data_path/themes/cybermind" "$instance_site/themes/" sed -i 's/^theme:.*/theme: cybermind/' "$instance_site/_config.yml" fi # Copy scaffolds if [ -d "$data_path/scaffolds" ]; then cp -r "$data_path/scaffolds/"* "$instance_site/scaffolds/" 2>/dev/null || true fi # Update config if [ -f "$instance_site/_config.yml" ]; then sed -i "s/^title:.*/title: $instance_title/" "$instance_site/_config.yml" sed -i "s|^url:.*|url: http://localhost:$instance_port|" "$instance_site/_config.yml" fi if [ "$was_stopped" = "1" ]; then lxc_stop fi log_info "Site created for instance '$instance'!" log_info "Start with: hexoctl instance start $instance" } cmd_site_delete() { require_root load_config local instance="${1:-default}" load_instance_config "$instance" || { log_error "Instance not found"; return 1; } if [ ! -d "$instance_site" ]; then log_error "No site exists for instance '$instance'" return 1 fi cmd_instance_stop "$instance" 2>/dev/null rm -rf "$instance_site" log_info "Site deleted for instance '$instance'" } # Content commands (instance-aware) cmd_new_post() { require_root load_config local title="$1" local instance="${2:-default}" [ -z "$title" ] && { log_error "Title required"; return 1; } load_instance_config "$instance" || { log_error "Instance not found"; return 1; } if ! lxc_running; then log_error "Container not running" return 1 fi lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && hexo new post \"$title\"" } cmd_new_page() { require_root load_config local title="$1" local instance="${2:-default}" [ -z "$title" ] && { log_error "Title required"; return 1; } load_instance_config "$instance" || { log_error "Instance not found"; return 1; } if ! lxc_running; then log_error "Container not running" return 1 fi lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && hexo new page \"$title\"" } cmd_new_draft() { require_root load_config local title="$1" local instance="${2:-default}" [ -z "$title" ] && { log_error "Title required"; return 1; } load_instance_config "$instance" || { log_error "Instance not found"; return 1; } if ! lxc_running; then log_error "Container not running" return 1 fi lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && hexo new draft \"$title\"" } cmd_list_posts() { load_config local instance="${1:-default}" load_instance_config "$instance" || { log_error "Instance not found"; return 1; } local posts_dir="$instance_site/source/_posts" [ -d "$posts_dir" ] || { echo "[]"; return; } echo "[" local first=1 for f in "$posts_dir"/*.md; do [ -f "$f" ] || continue local filename=$(basename "$f") local slug="${filename%.md}" local title=$(grep -m1 "^title:" "$f" | sed 's/^title:[[:space:]]*//' | tr -d '"' | tr -d "'") [ "$first" = "1" ] || echo "," first=0 echo " {\"slug\": \"$slug\", \"title\": \"$title\"}" done echo "]" } cmd_list_drafts() { load_config local instance="${1:-default}" load_instance_config "$instance" || { log_error "Instance not found"; return 1; } local drafts_dir="$instance_site/source/_drafts" [ -d "$drafts_dir" ] || { echo "[]"; return; } echo "[" local first=1 for f in "$drafts_dir"/*.md; do [ -f "$f" ] || continue local filename=$(basename "$f") local slug="${filename%.md}" local title=$(grep -m1 "^title:" "$f" | sed 's/^title:[[:space:]]*//' | tr -d '"' | tr -d "'") [ "$first" = "1" ] || echo "," first=0 echo " {\"slug\": \"$slug\", \"title\": \"$title\"}" done echo "]" } # Build commands (instance-aware) cmd_serve() { require_root load_config local instance="${1:-default}" load_instance_config "$instance" || { log_error "Instance not found"; return 1; } if ! lxc_running; then log_error "Container not running" return 1 fi log_info "Starting preview server for '$instance' on port $instance_port..." lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && hexo server -p $instance_port -i 0.0.0.0" } cmd_build() { require_root load_config local instance="${1:-default}" load_instance_config "$instance" || { log_error "Instance not found"; return 1; } if ! lxc_running; then log_error "Container not running" return 1 fi log_info "Generating static files for '$instance'..." lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && hexo generate" log_info "Build complete!" } cmd_clean() { require_root load_config local instance="${1:-default}" load_instance_config "$instance" || { log_error "Instance not found"; return 1; } if ! lxc_running; then log_error "Container not running" return 1 fi log_info "Cleaning generated files for '$instance'..." lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && hexo clean" } cmd_publish() { require_root load_config local instance="${1:-default}" load_instance_config "$instance" || { log_error "Instance not found"; return 1; } local public_dir="$instance_site/public" local portal_path="$(uci_get ${instance}.publish_path)" || portal_path="/www/${instance}" if ! lxc_running; then log_error "Container not running" return 1 fi # Calculate web root local web_root="${portal_path#/www}" [ -z "$web_root" ] && web_root="/" [ "${web_root%/}" = "$web_root" ] && web_root="$web_root/" log_info "Setting root to: $web_root" # Update config sed -i "s|^root:.*|root: $web_root|" "$instance_site/_config.yml" log_info "Regenerating..." lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && hexo clean && hexo generate" [ -d "$public_dir" ] || { log_error "Build failed"; return 1; } log_info "Publishing to $portal_path..." ensure_dir "$portal_path" rsync -av --delete "$public_dir/" "$portal_path/" log_info "Published $(find "$portal_path" -type f | wc -l) files" } cmd_logs() { load_config local instance="${1:-default}" if lxc_running; then lxc_exec cat /var/log/hexo/$instance.log 2>/dev/null || echo "No logs for '$instance'" else echo "Container not running" fi } cmd_shell() { require_root if ! lxc_running; then log_error "Container not running" exit 1 fi lxc-attach -n "$LXC_NAME" -- /bin/sh } cmd_exec() { require_root lxc_exec "$@" } cmd_service_run() { require_root load_config lxc_check_prereqs || exit 1 lxc_run } cmd_service_stop() { require_root lxc_stop } # Gitea integration (instance-aware) cmd_gitea_setup() { require_root load_config local instance="${1:-default}" if [ -z "$gitea_token" ]; then log_error "Gitea token not configured" return 1 fi if ! lxc_running; then log_error "Container not running" return 1 fi log_info "Configuring git credentials..." local gitea_host=$(echo "$gitea_url" | sed 's|^https\?://||' | sed 's|/.*||') lxc_exec sh -c " export PATH=/usr/local/bin:\$PATH git config --global user.name '$gitea_user' git config --global user.email '${gitea_user}@localhost' git config --global credential.helper store rm -rf ~/.git-credentials cat > ~/.git-credentials << CRED https://${gitea_user}:${gitea_token}@${gitea_host} http://${gitea_user}:${gitea_token}@${gitea_host} CRED chmod 600 ~/.git-credentials " log_info "Git credentials configured" } cmd_gitea_clone() { require_root load_config local instance="${1:-default}" load_instance_config "$instance" || { log_error "Instance not found"; return 1; } if [ "$gitea_enabled" != "1" ]; then log_error "Gitea integration not enabled" return 1 fi if ! lxc_running; then log_error "Container not running" return 1 fi local content_path="$instance_path/content" if [ -d "$content_path/.git" ]; then log_info "Content repo already cloned, pulling..." cd "$content_path" && git pull else log_info "Cloning content repo..." local gitea_host=$(echo "$gitea_url" | sed 's|^https\?://||' | sed 's|/.*||') local clone_url="http://${gitea_user}:${gitea_token}@${gitea_host}/${gitea_user}/${gitea_content_repo}.git" ensure_dir "$(dirname "$content_path")" rm -rf "$content_path" git clone -b "$gitea_content_branch" "$clone_url" "$content_path" || { log_error "Failed to clone" return 1 } fi # Check if content is a full hexo site if [ -f "$content_path/package.json" ] && [ -d "$content_path/source" ]; then log_info "Content is a complete Hexo site, linking..." lxc_exec sh -c " rm -rf /opt/hexojs/instances/$instance/site ln -sf /opt/hexojs/instances/$instance/content /opt/hexojs/instances/$instance/site cd /opt/hexojs/instances/$instance/site && npm install " fi log_info "Content cloned for instance '$instance'" } cmd_gitea_sync() { require_root load_config local instance="${1:-default}" load_instance_config "$instance" || { log_error "Instance not found"; return 1; } local content_path="$instance_path/content" [ -d "$content_path/.git" ] || { log_error "Content not cloned"; return 1; } log_info "Pulling latest content..." cd "$content_path" && git pull log_info "Content synced for '$instance'" } cmd_gitea_push() { require_root load_config local instance="${1:-default}" local message="${2:-Auto-commit from SecuBox}" load_instance_config "$instance" || { log_error "Instance not found"; return 1; } local content_path="$instance_path/content" [ -d "$content_path/.git" ] || { log_error "Content not cloned"; return 1; } log_info "Pushing changes for '$instance'..." cd "$content_path" git add -A git commit -m "$message" 2>/dev/null || log_info "Nothing to commit" git push log_info "Content pushed for '$instance'" } # GitHub integration (public repos) cmd_github_clone() { require_root load_config local repo_url="$1" local instance="${2:-default}" local branch="${3:-main}" [ -z "$repo_url" ] && { log_error "GitHub repo URL required"; return 1; } load_instance_config "$instance" || { log_info "Creating instance '$instance'..." cmd_instance_create "$instance" load_instance_config "$instance" } if ! lxc_running; then log_error "Container not running" return 1 fi local content_path="$instance_path/content" log_info "Cloning from GitHub: $repo_url (branch: $branch)" ensure_dir "$(dirname "$content_path")" rm -rf "$content_path" git clone -b "$branch" "$repo_url" "$content_path" || { log_error "Failed to clone from GitHub" return 1 } # Check if content is a full hexo site if [ -f "$content_path/package.json" ] && [ -d "$content_path/source" ]; then log_info "Content is a complete Hexo site, linking..." lxc_exec sh -c " rm -rf /opt/hexojs/instances/$instance/site ln -sf /opt/hexojs/instances/$instance/content /opt/hexojs/instances/$instance/site cd /opt/hexojs/instances/$instance/site && npm install " fi log_info "GitHub repo cloned for instance '$instance'" } # Backup/Restore commands cmd_backup() { require_root load_config local instance="${1:-default}" local backup_name="${2:-$(date +%Y%m%d-%H%M%S)}" load_instance_config "$instance" || { log_error "Instance not found"; return 1; } local backup_dir="$data_path/backups" ensure_dir "$backup_dir" local backup_file="$backup_dir/${instance}_${backup_name}.tar.gz" if [ ! -d "$instance_site" ]; then log_error "No site to backup for instance '$instance'" return 1 fi log_info "Creating backup: $backup_file" # Backup site directory and config tar -czf "$backup_file" \ -C "$instance_path" site \ -C /etc/config hexojs 2>/dev/null || { log_error "Backup failed" return 1 } local size=$(du -h "$backup_file" | cut -f1) log_info "Backup created: $backup_file ($size)" } cmd_restore() { require_root load_config local backup_name="$1" local instance="${2:-default}" [ -z "$backup_name" ] && { log_error "Backup name required"; return 1; } local backup_dir="$data_path/backups" local backup_file="$backup_dir/$backup_name" # Try with and without .tar.gz extension [ -f "$backup_file" ] || backup_file="$backup_dir/${backup_name}.tar.gz" [ -f "$backup_file" ] || backup_file="$backup_dir/${instance}_${backup_name}.tar.gz" [ -f "$backup_file" ] || { log_error "Backup not found: $backup_name"; return 1; } load_instance_config "$instance" || { log_info "Creating instance '$instance'..." cmd_instance_create "$instance" load_instance_config "$instance" } # Stop instance if running cmd_instance_stop "$instance" 2>/dev/null log_info "Restoring from: $backup_file" # Remove existing site rm -rf "$instance_site" ensure_dir "$instance_path" # Extract backup tar -xzf "$backup_file" -C "$instance_path" || { log_error "Restore failed" return 1 } log_info "Backup restored for instance '$instance'" log_info "Start with: hexoctl instance start $instance" } cmd_backup_list() { load_config local backup_dir="$data_path/backups" [ -d "$backup_dir" ] || { echo "[]"; return; } echo "[" local first=1 for f in "$backup_dir"/*.tar.gz; do [ -f "$f" ] || continue local name=$(basename "$f" .tar.gz) local size=$(du -h "$f" | cut -f1) local ts=$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f" 2>/dev/null) [ "$first" = "1" ] || echo "," first=0 printf ' {"name": "%s", "size": "%s", "timestamp": %s}' "$name" "$size" "${ts:-0}" done echo "" echo "]" } cmd_backup_delete() { require_root load_config local backup_name="$1" [ -z "$backup_name" ] && { log_error "Backup name required"; return 1; } local backup_dir="$data_path/backups" local backup_file="$backup_dir/$backup_name" [ -f "$backup_file" ] || backup_file="$backup_dir/${backup_name}.tar.gz" [ -f "$backup_file" ] || { log_error "Backup not found: $backup_name"; return 1; } rm -f "$backup_file" log_info "Backup deleted: $backup_name" } # Quick publish (build + deploy in one command) cmd_quick_publish() { require_root load_config local instance="${1:-default}" load_instance_config "$instance" || { log_error "Instance not found"; return 1; } if ! lxc_running; then log_error "Container not running" return 1 fi log_info "Quick publish for '$instance'..." # Clean, build, publish lxc_exec sh -c "cd /opt/hexojs/instances/$instance/site && hexo clean && hexo generate" || { log_error "Build failed" return 1 } cmd_publish "$instance" log_info "Quick publish complete!" } # Status JSON (for RPCD) cmd_status_json() { load_config local enabled="$(uci_get main.enabled)" local running="false" lxc_running && running="true" local installed="false" lxc_exists && installed="true" cat << EOF { "enabled": $([ "$enabled" = "1" ] && echo true || echo false), "running": $running, "installed": $installed, "data_path": "$data_path", "memory_limit": "$memory_limit", "instances": [ EOF local first=1 for instance in $(get_enabled_instances); do load_instance_config "$instance" || continue local inst_running="false" if [ "$running" = "true" ]; then local pid=$(lxc_exec cat /var/run/hexo/$instance.pid 2>/dev/null) [ -n "$pid" ] && inst_running="true" fi local site_exists="false" [ -d "$instance_site" ] && site_exists="true" [ "$first" = "1" ] || printf "," first=0 cat << EOF { "name": "$instance", "enabled": $([ "$instance_enabled" = "1" ] && echo true || echo false), "running": $inst_running, "port": $instance_port, "title": "$instance_title", "theme": "$instance_theme", "site_exists": $site_exists } EOF done echo " ]" echo "}" } # =============================================== # User Management Commands # =============================================== HTPASSWD_FILE="/etc/hexojs/htpasswd" # Generate password hash (apr1 format for HAProxy) generate_password_hash() { local password="$1" # Use openssl for apr1 hash openssl passwd -apr1 "$password" 2>/dev/null || \ openssl passwd -1 "$password" 2>/dev/null || \ echo "$password" # fallback to plain (not recommended) } cmd_user_add() { require_root local username="$1" local password="$2" [ -z "$username" ] && { log_error "Username required"; return 1; } # Validate username echo "$username" | grep -qE '^[a-z][a-z0-9_]*$' || { log_error "Invalid username. Use lowercase letters, numbers, underscore." return 1 } # Check if user exists local existing=$(uci_get "user_$username") [ -n "$existing" ] && { log_error "User '$username' already exists"; return 1; } # Generate password if not provided [ -z "$password" ] && { password=$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 12) log_info "Generated password: $password" } # Create password hash local hash=$(generate_password_hash "$password") # Create UCI user section uci set hexojs.user_${username}=user uci set hexojs.user_${username}.password_hash="$hash" uci set hexojs.user_${username}.role='editor' uci commit hexojs # Update htpasswd file cmd_update_htpasswd log_info "User '$username' created" } cmd_user_del() { require_root local username="$1" [ -z "$username" ] && { log_error "Username required"; return 1; } # Remove from UCI uci delete hexojs.user_${username} 2>/dev/null || { log_error "User '$username' not found" return 1 } uci commit hexojs # Update htpasswd file cmd_update_htpasswd log_info "User '$username' deleted" } # Sync users from SecuBox/LuCI (rpcd config) cmd_user_sync_secubox() { require_root log_info "Syncing users from SecuBox/LuCI..." local added=0 local skipped=0 # Read all login sections from rpcd config for section in $(uci show rpcd 2>/dev/null | grep '=login$' | cut -d'.' -f2 | cut -d'=' -f1); do local username=$(uci -q get rpcd.${section}.username) [ -z "$username" ] && continue # Skip if already exists in hexojs local existing=$(uci_get "user_$username" 2>/dev/null) if [ -n "$existing" ]; then log_info " Skip: $username (already exists)" skipped=$((skipped + 1)) continue fi # Get password - format is $p$ or direct hash local pw_ref=$(uci -q get rpcd.${section}.password) # If $p$username format, get hash from /etc/shadow local hash="" if echo "$pw_ref" | grep -q '^\$p\$'; then local sys_user="${pw_ref#\$p\$}" # Extract hash from shadow file local shadow_hash=$(grep "^${sys_user}:" /etc/shadow 2>/dev/null | cut -d: -f2) if [ -n "$shadow_hash" ] && [ "$shadow_hash" != "*" ] && [ "$shadow_hash" != "x" ]; then hash="$shadow_hash" fi else # Direct hash in rpcd config hash="$pw_ref" fi if [ -z "$hash" ]; then log_warn " Skip: $username (no password hash found)" skipped=$((skipped + 1)) continue fi # Create HexoJS user with the same hash uci set hexojs.user_${username}=user uci set hexojs.user_${username}.password_hash="$hash" uci set hexojs.user_${username}.role='editor' uci set hexojs.user_${username}.source='secubox' log_info " Added: $username" added=$((added + 1)) done uci commit hexojs # Update htpasswd file cmd_update_htpasswd log_info "Sync complete: $added added, $skipped skipped" } cmd_user_list() { load_config echo "HexoJS Users:" echo "-------------" local found=0 for section in $(uci show hexojs 2>/dev/null | grep '=user$' | cut -d'.' -f2 | cut -d'=' -f1); do found=1 local username="${section#user_}" local role=$(uci_get "${section}.role") local instances=$(uci_get "${section}.instances") printf " %-15s role:%-8s instances: %s\n" "$username" "${role:-editor}" "${instances:-all}" done [ "$found" = "0" ] && echo " (no users)" } cmd_user_passwd() { require_root local username="$1" local password="$2" [ -z "$username" ] && { log_error "Username required"; return 1; } # Check if user exists local existing=$(uci_get "user_$username") [ -z "$existing" ] && { log_error "User '$username' not found"; return 1; } # Generate password if not provided [ -z "$password" ] && { password=$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 12) log_info "Generated password: $password" } # Create password hash local hash=$(generate_password_hash "$password") # Update UCI uci set hexojs.user_${username}.password_hash="$hash" uci commit hexojs # Update htpasswd file cmd_update_htpasswd log_info "Password changed for '$username'" } cmd_user_grant() { require_root local username="$1" local instance="$2" [ -z "$username" ] && { log_error "Username required"; return 1; } [ -z "$instance" ] && { log_error "Instance required"; return 1; } # Check if user exists local existing=$(uci_get "user_$username") [ -z "$existing" ] && { log_error "User '$username' not found"; return 1; } # Add instance to user's list uci add_list hexojs.user_${username}.instances="$instance" uci commit hexojs log_info "Granted '$username' access to '$instance'" } cmd_user_revoke() { require_root local username="$1" local instance="$2" [ -z "$username" ] && { log_error "Username required"; return 1; } [ -z "$instance" ] && { log_error "Instance required"; return 1; } # Remove instance from user's list uci del_list hexojs.user_${username}.instances="$instance" 2>/dev/null uci commit hexojs log_info "Revoked '$username' access from '$instance'" } cmd_update_htpasswd() { local htpasswd_dir=$(dirname "$HTPASSWD_FILE") ensure_dir "$htpasswd_dir" > "$HTPASSWD_FILE" for section in $(uci show hexojs 2>/dev/null | grep '=user$' | cut -d'.' -f2 | cut -d'=' -f1); do local username="${section#user_}" local hash=$(uci_get "${section}.password_hash") [ -n "$hash" ] && echo "${username}:${hash}" >> "$HTPASSWD_FILE" done chmod 600 "$HTPASSWD_FILE" } # =============================================== # Authentication Commands # =============================================== cmd_auth_enable() { require_root load_config local instance="${1:-default}" load_instance_config "$instance" || { log_error "Instance not found"; return 1; } uci set hexojs.${instance}.auth_enabled='1' uci commit hexojs log_info "Authentication enabled for '$instance'" log_info "Configure HAProxy with: hexoctl auth haproxy $instance" } cmd_auth_disable() { require_root load_config local instance="${1:-default}" load_instance_config "$instance" || { log_error "Instance not found"; return 1; } uci set hexojs.${instance}.auth_enabled='0' uci commit hexojs log_info "Authentication disabled for '$instance'" } cmd_auth_status() { load_config local instance="${1:-}" if [ -n "$instance" ]; then load_instance_config "$instance" || { log_error "Instance not found"; return 1; } local auth_enabled=$(uci_get "${instance}.auth_enabled") local auth_users=$(uci_get "${instance}.auth_users") echo "Instance: $instance" echo "Auth Enabled: $([ "$auth_enabled" = "1" ] && echo "yes" || echo "no")" echo "Allowed Users: ${auth_users:-all}" else echo "Authentication Status:" echo "---------------------" for inst in $(get_enabled_instances); do load_instance_config "$inst" || continue local auth_enabled=$(uci_get "${inst}.auth_enabled") printf " %-15s %s\n" "$inst" "$([ "$auth_enabled" = "1" ] && echo "[AUTH]" || echo "[PUBLIC]")" done echo "" echo "Users:" cmd_user_list | tail -n +3 fi } cmd_auth_haproxy() { require_root load_config local instance="${1:-default}" load_instance_config "$instance" || { log_error "Instance not found"; return 1; } local auth_enabled=$(uci_get "${instance}.auth_enabled") [ "$auth_enabled" != "1" ] && { log_error "Auth not enabled for '$instance'"; return 1; } # Update htpasswd cmd_update_htpasswd local domain=$(uci_get "${instance}.domain") [ -z "$domain" ] && domain="${instance}.hexo.local" log_info "HAProxy configuration for authenticated instance '$instance':" echo "" echo "# Add to HAProxy frontend https-in:" echo " acl host_${instance}_hexo hdr(host) -i ${domain}" echo " http-request auth realm \"SecuBox HexoJS\" if host_${instance}_hexo !{ http_auth(hexojs_users) }" echo " use_backend hexo_${instance} if host_${instance}_hexo" echo "" echo "# Add userlist section:" echo "userlist hexojs_users" for section in $(uci show hexojs 2>/dev/null | grep '=user$' | cut -d'.' -f2 | cut -d'=' -f1); do local username="${section#user_}" local hash=$(uci_get "${section}.password_hash") [ -n "$hash" ] && echo " user ${username} password ${hash}" done echo "" echo "# Add backend:" echo "backend hexo_${instance}" echo " mode http" echo " server hexo 127.0.0.1:${instance_port} check" echo "" log_info "Copy the above to your HAProxy config and reload" } # Generate full HAProxy config for all authenticated instances cmd_auth_haproxy_all() { require_root load_config echo "# HexoJS Authenticated Instances - HAProxy Config" echo "# Generated: $(date)" echo "" echo "userlist hexojs_users" for section in $(uci show hexojs 2>/dev/null | grep '=user$' | cut -d'.' -f2 | cut -d'=' -f1); do local username="${section#user_}" local hash=$(uci_get "${section}.password_hash") [ -n "$hash" ] && echo " user ${username} password ${hash}" done echo "" for inst in $(get_enabled_instances); do load_instance_config "$inst" || continue local auth_enabled=$(uci_get "${inst}.auth_enabled") [ "$auth_enabled" != "1" ] && continue local domain=$(uci_get "${inst}.domain") [ -z "$domain" ] && domain="${inst}.hexo.local" echo "# Instance: $inst (port $instance_port)" echo "acl host_${inst}_hexo hdr(host) -i ${domain}" echo "http-request auth realm \"SecuBox HexoJS\" if host_${inst}_hexo !{ http_auth(hexojs_users) }" echo "use_backend hexo_${inst} if host_${inst}_hexo" echo "" echo "backend hexo_${inst}" echo " mode http" echo " server hexo 127.0.0.1:${instance_port} check" echo "" done } # Apply auth configuration to HAProxy (auto-configure) cmd_auth_apply() { require_root load_config local instance="${1:-}" local domain="${2:-}" [ -z "$instance" ] && { log_error "Instance name required"; return 1; } [ -z "$domain" ] && domain="${instance}.secubox.local" local auth_enabled=$(uci_get "${instance}.auth_enabled") [ "$auth_enabled" != "1" ] && { log_info "Enabling auth for '$instance'..." uci set hexojs.${instance}.auth_enabled='1' uci commit hexojs } # Save domain uci set hexojs.${instance}.domain="$domain" uci commit hexojs # Update htpasswd cmd_update_htpasswd # Write userlist file for HAProxy include local userlist_file="/srv/haproxy/config/hexojs-users.cfg" log_info "Writing userlist to $userlist_file..." cat > "$userlist_file" << 'USERLIST_HEAD' # HexoJS Users - Auto-generated by hexoctl # Include this in haproxy.cfg before frontends USERLIST_HEAD echo "userlist hexojs_users" >> "$userlist_file" for section in $(uci show hexojs 2>/dev/null | grep '=user$' | cut -d'.' -f2 | cut -d'=' -f1); do local username="${section#user_}" local hash=$(uci_get "${section}.password_hash") [ -n "$hash" ] && echo " user ${username} password ${hash}" >> "$userlist_file" done # Check if haproxyctl exists if command -v haproxyctl >/dev/null 2>&1; then # Add backend pointing to static files (served by uhttpd) local static_url="/static/${instance}/" local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.255.1") log_info "Configuring HAProxy vhost: $domain" # Remove existing vhost if present haproxyctl vhost del "$domain" 2>/dev/null # Add vhost pointing to uhttpd haproxyctl vhost add "$domain" uhttpd "$router_ip" 80 2>/dev/null || true # The auth rules need to be added to HAProxy config manually or via template log_info "Vhost created: $domain -> uhttpd" log_info "" log_info "Add these lines to HAProxy frontend https-in:" echo "" echo " # HexoJS Auth for $instance" echo " acl host_${instance}_hexo hdr(host) -i ${domain}" echo " http-request auth realm \"SecuBox HexoJS\" if host_${instance}_hexo !{ http_auth(hexojs_users) }" echo "" log_info "Then reload HAProxy: haproxyctl reload" else log_warn "haproxyctl not found" log_info "Manual HAProxy config required - see: hexoctl auth haproxy $instance" fi log_info "Auth applied for '$instance' at $domain" } # =============================================== # KISS Static Upload (No Hexo Build Required) # =============================================== # Create a static-only instance (no Hexo, just serves files) cmd_static_create() { require_root load_config local name="$1" [ -z "$name" ] && { log_error "Instance name required"; return 1; } # Validate name echo "$name" | grep -qE '^[a-z][a-z0-9_]*$' || { log_error "Invalid name. Use lowercase letters, numbers, underscore." return 1 } # Find next available port local port=4000 while uci show hexojs 2>/dev/null | grep -q "port='$port'"; do port=$((port + 1)) done # Create UCI config uci set hexojs.${name}=instance uci set hexojs.${name}.enabled='1' uci set hexojs.${name}.port="$port" uci set hexojs.${name}.title="$name" uci set hexojs.${name}.type='static' uci commit hexojs # Create directory structure local static_dir="$data_path/static/$name" ensure_dir "$static_dir" # Create default index.html cat > "$static_dir/index.html" << 'HTML' SecuBox Static Site

SecuBox Static Site

Upload your HTML files to get started

hexoctl static upload <file.html> [instance]
HTML log_info "Static instance '$name' created on port $port" log_info "Directory: $static_dir" log_info "Upload files: hexoctl static upload $name" } # Upload file to static instance (KISS fast publish) cmd_static_upload() { require_root load_config local file="$1" local instance="${2:-default}" [ -z "$file" ] && { log_error "File path required"; return 1; } [ -f "$file" ] || { log_error "File not found: $file"; return 1; } local static_dir="$data_path/static/$instance" [ -d "$static_dir" ] || { log_info "Creating static instance '$instance'..." cmd_static_create "$instance" } local filename=$(basename "$file") cp "$file" "$static_dir/$filename" log_info "Uploaded: $filename → $static_dir/" log_info "URL: http://$(uci -q get network.lan.ipaddr || echo localhost):$(uci_get ${instance}.port || echo 4000)/$filename" } # List files in static instance cmd_static_list() { load_config local instance="${1:-}" if [ -n "$instance" ]; then local static_dir="$data_path/static/$instance" [ -d "$static_dir" ] || { log_error "Static instance '$instance' not found"; return 1; } echo "Files in '$instance':" ls -lh "$static_dir" | tail -n +2 else echo "Static Instances:" echo "-----------------" for dir in "$data_path/static"/*; do [ -d "$dir" ] || continue local name=$(basename "$dir") local count=$(find "$dir" -type f | wc -l) local port=$(uci_get "${name}.port") printf " %-15s %3d files port:%s\n" "$name" "$count" "${port:-?}" done fi } # Start simple static server (no Hexo, just Python/BusyBox httpd) cmd_static_serve() { require_root load_config local instance="${1:-default}" local static_dir="$data_path/static/$instance" [ -d "$static_dir" ] || { log_error "Static instance '$instance' not found"; return 1; } local port=$(uci_get "${instance}.port") [ -z "$port" ] && port=4000 log_info "Serving '$instance' on port $port..." # Try Python first, fallback to busybox httpd if command -v python3 >/dev/null 2>&1; then cd "$static_dir" && python3 -m http.server "$port" --bind 0.0.0.0 elif command -v busybox >/dev/null 2>&1; then busybox httpd -f -p "$port" -h "$static_dir" else log_error "No HTTP server available (need python3 or busybox)" return 1 fi } # Delete static instance cmd_static_delete() { require_root load_config local name="$1" [ -z "$name" ] && { log_error "Instance name required"; return 1; } local static_dir="$data_path/static/$name" [ -d "$static_dir" ] || { log_error "Static instance '$name' not found"; return 1; } # Remove directory rm -rf "$static_dir" # Remove UCI config if exists uci delete hexojs.${name} 2>/dev/null uci commit hexojs 2>/dev/null log_info "Static instance '$name' deleted" } # Publish static instance to /www/ (immediate serving via uhttpd) cmd_static_publish() { require_root load_config local instance="${1:-default}" local static_dir="$data_path/static/$instance" [ -d "$static_dir" ] || { log_error "Static instance '$instance' not found"; return 1; } local publish_path="/www/static/${instance}" ensure_dir "$publish_path" log_info "Publishing to $publish_path..." rsync -av --delete "$static_dir/" "$publish_path/" local file_count=$(find "$publish_path" -type f | wc -l) log_info "Published $file_count files" log_info "URL: http://$(uci -q get network.lan.ipaddr || echo localhost)/static/${instance}/" } # Quick upload + publish workflow (KISS one-liner) cmd_static_quick() { require_root load_config local file="$1" local instance="${2:-quick}" [ -z "$file" ] && { log_error "File path required"; return 1; } [ -f "$file" ] || { log_error "File not found: $file"; return 1; } # Upload cmd_static_upload "$file" "$instance" # Publish immediately cmd_static_publish "$instance" } cmd_instance_list_json() { load_config echo "[" local first=1 for section in $(uci show hexojs 2>/dev/null | grep '=instance$' | cut -d'.' -f2 | cut -d'=' -f1); do load_instance_config "$section" local running="false" if lxc_running && [ -f "$LXC_ROOTFS/var/run/hexo/${section}.pid" ]; then running="true" fi local site_exists="false" [ -d "$instance_site" ] && site_exists="true" [ "$first" = "1" ] || echo "," first=0 cat << EOF { "name": "$section", "enabled": $([ "$instance_enabled" = "1" ] && echo true || echo false), "running": $running, "port": $instance_port, "title": "$instance_title", "theme": "$instance_theme", "site_exists": $site_exists } EOF done echo "]" } # Main case "${1:-}" in install) shift; cmd_install "$@" ;; uninstall) shift; cmd_uninstall "$@" ;; update) shift; cmd_update "$@" ;; status) shift; cmd_status "$@" ;; instance) shift case "${1:-}" in list) shift; cmd_instance_list "$@" ;; create) shift; cmd_instance_create "$@" ;; delete) shift; cmd_instance_delete "$@" ;; start) shift; cmd_instance_start "$@" ;; stop) shift; cmd_instance_stop "$@" ;; status) shift; cmd_instance_status "$@" ;; *) echo "Usage: hexoctl instance {list|create|delete|start|stop|status} [name]" ;; esac ;; site) shift case "${1:-}" in create) shift; cmd_site_create "$@" ;; delete) shift; cmd_site_delete "$@" ;; list) shift; cmd_instance_list "$@" ;; *) echo "Usage: hexoctl site {create|delete|list} [instance]" ;; esac ;; new) shift case "${1:-}" in post) shift; cmd_new_post "$@" ;; page) shift; cmd_new_page "$@" ;; draft) shift; cmd_new_draft "$@" ;; *) echo "Usage: hexoctl new {post|page|draft} \"Title\" [instance]" ;; esac ;; list) shift case "${1:-}" in posts) shift; cmd_list_posts "$@" ;; drafts) shift; cmd_list_drafts "$@" ;; *) echo "Usage: hexoctl list {posts|drafts} [instance]" ;; esac ;; serve) shift; cmd_serve "$@" ;; build|generate) shift; cmd_build "$@" ;; clean) shift; cmd_clean "$@" ;; publish) shift; cmd_publish "$@" ;; logs) shift; cmd_logs "$@" ;; shell) shift; cmd_shell "$@" ;; exec) shift; cmd_exec "$@" ;; service-run) shift; cmd_service_run "$@" ;; service-stop) shift; cmd_service_stop "$@" ;; gitea) shift case "${1:-}" in setup) shift; cmd_gitea_setup "$@" ;; clone) shift; cmd_gitea_clone "$@" ;; sync) shift; cmd_gitea_sync "$@" ;; push) shift; cmd_gitea_push "$@" ;; *) echo "Usage: hexoctl gitea {setup|clone|sync|push} [instance] [message]" ;; esac ;; github) shift case "${1:-}" in clone) shift; cmd_github_clone "$@" ;; *) echo "Usage: hexoctl github clone [instance] [branch]" ;; esac ;; backup) shift case "${1:-}" in list) shift; cmd_backup_list "$@" ;; create) shift; cmd_backup "$@" ;; delete) shift; cmd_backup_delete "$@" ;; restore) shift; cmd_restore "$@" ;; "") cmd_backup "$@" ;; *) cmd_backup "$@" ;; esac ;; restore) shift; cmd_restore "$@" ;; quick-publish) shift; cmd_quick_publish "$@" ;; status-json) cmd_status_json ;; instance-list-json) cmd_instance_list_json ;; user) shift case "${1:-}" in add) shift; cmd_user_add "$@" ;; del|delete) shift; cmd_user_del "$@" ;; list) shift; cmd_user_list "$@" ;; passwd) shift; cmd_user_passwd "$@" ;; grant) shift; cmd_user_grant "$@" ;; revoke) shift; cmd_user_revoke "$@" ;; sync-secubox|sync) shift; cmd_user_sync_secubox "$@" ;; *) echo "Usage: hexoctl user {add|del|list|passwd|grant|revoke|sync-secubox}" ;; esac ;; auth) shift case "${1:-}" in enable) shift; cmd_auth_enable "$@" ;; disable) shift; cmd_auth_disable "$@" ;; status) shift; cmd_auth_status "$@" ;; haproxy) shift; cmd_auth_haproxy "$@" ;; haproxy-all) cmd_auth_haproxy_all ;; apply) shift; cmd_auth_apply "$@" ;; *) echo "Usage: hexoctl auth {enable|disable|status|haproxy|apply} [instance]" ;; esac ;; # KISS Static Upload (no Hexo, direct HTML) static) shift case "${1:-}" in create) shift; cmd_static_create "$@" ;; upload) shift; cmd_static_upload "$@" ;; publish) shift; cmd_static_publish "$@" ;; quick) shift; cmd_static_quick "$@" ;; list) shift; cmd_static_list "$@" ;; serve) shift; cmd_static_serve "$@" ;; delete) shift; cmd_static_delete "$@" ;; *) echo "Usage: hexoctl static {create|upload|publish|quick|list|serve|delete} [instance]" ;; esac ;; *) usage ;; esac