Matrix Homeserver (Conduit): - E2EE mesh messaging using Conduit v0.10.12 in LXC container - matrixctl CLI: install/uninstall, user/room management, federation - luci-app-matrix: status cards, user form, emancipate, mesh publish - RPCD backend with 17 methods - Identity (DID) integration and P2P mesh publication SaaS Relay CDN Caching & Session Replay: - CDN cache profiles: minimal, gandalf (default), aggressive - Session replay modes: shared, per_user, master - saasctl cache/session commands for management - Enhanced mitmproxy addon (415 lines) with response caching Media Services Hub Dashboard: - Unified dashboard at /admin/services/media-hub - Category-organized cards (streaming, conferencing, apps, etc.) - Service status indicators with start/stop/restart controls - RPCD backend querying 8 media services Also includes: - HexoJS static upload workflow and multi-user auth - Jitsi config.js Promise handling fix - Feed package updates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2316 lines
60 KiB
Bash
2316 lines
60 KiB
Bash
#!/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 <<EOF
|
|
SecuBox Hexo CMS Controller (Multi-Instance)
|
|
|
|
Usage: $(basename $0) <command> [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 <name> Create new instance
|
|
instance delete <name> Delete an instance
|
|
instance start <name> Start instance server
|
|
instance stop <name> Stop instance server
|
|
instance status <name> 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 <url> [instance] [branch] Clone from GitHub
|
|
|
|
Backup/Restore:
|
|
backup [instance] [name] Create backup
|
|
backup list List all backups
|
|
backup delete <name> Delete a backup
|
|
restore <name> [instance] Restore from backup
|
|
|
|
Quick Commands:
|
|
quick-publish [instance] Clean, build, and publish
|
|
|
|
User Management:
|
|
user add <name> [password] Add user with password
|
|
user del <name> Delete user
|
|
user list List all users
|
|
user passwd <name> [pass] Change user password
|
|
user grant <name> <inst> Grant access to instance
|
|
user revoke <name> <inst> Revoke access to instance
|
|
user sync-secubox Import users from SecuBox/LuCI
|
|
|
|
Authentication:
|
|
auth enable <instance> Enable auth for instance
|
|
auth disable <instance> Disable auth for instance
|
|
auth status [instance] Show auth status
|
|
auth haproxy <instance> Generate HAProxy auth ACLs
|
|
auth apply <inst> [domain] Auto-configure HAProxy with auth
|
|
|
|
KISS Static Sites (No Hexo Build):
|
|
static create <name> Create static-only site
|
|
static upload <file> [inst] Upload HTML/CSS/JS directly
|
|
static publish [instance] Publish to /www/ for immediate serving
|
|
static quick <file> [inst] Upload + publish in one step
|
|
static list [instance] List static files
|
|
static serve [instance] Serve static files (Python/httpd)
|
|
static delete <name> Delete static instance
|
|
|
|
Utility:
|
|
shell Open shell in container
|
|
logs [instance] View logs
|
|
exec <cmd> 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$<system_user> 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'
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>SecuBox Static Site</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #e0e0e0;
|
|
}
|
|
.container {
|
|
text-align: center;
|
|
padding: 40px;
|
|
background: rgba(255,255,255,0.05);
|
|
border-radius: 20px;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
}
|
|
h1 { font-size: 2.5em; margin-bottom: 20px; color: #f97316; }
|
|
p { color: #999; margin: 10px 0; }
|
|
.upload-hint {
|
|
margin-top: 30px;
|
|
padding: 15px;
|
|
background: rgba(249,115,22,0.1);
|
|
border-radius: 10px;
|
|
font-family: monospace;
|
|
font-size: 0.9em;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>SecuBox Static Site</h1>
|
|
<p>Upload your HTML files to get started</p>
|
|
<div class="upload-hint">
|
|
hexoctl static upload <file.html> [instance]
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
HTML
|
|
|
|
log_info "Static instance '$name' created on port $port"
|
|
log_info "Directory: $static_dir"
|
|
log_info "Upload files: hexoctl static upload <file> $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 <repo_url> [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
|