#!/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 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'" } # 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 "$@" ;; *) echo "Usage: hexoctl gitea {setup|clone|sync} [instance]" ;; esac ;; *) usage ;; esac