#!/bin/sh # SecuBox Metabolizer Blog Pipeline Controller # Copyright (C) 2025 CyberMind.fr # # Integrates Gitea + Streamlit + HexoJS for blog CMS CONFIG="metabolizer" # Logging log_info() { echo "[INFO] $*"; logger -t metabolizer "$*"; } log_error() { echo "[ERROR] $*" >&2; logger -t metabolizer -p err "$*"; } log_debug() { [ "$DEBUG" = "1" ] && echo "[DEBUG] $*"; } # Helpers require_root() { [ "$(id -u)" -eq 0 ] || { log_error "This command requires root privileges" exit 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 configuration load_config() { # Main settings enabled="$(uci_get main.enabled)" || enabled="0" gitea_url="$(uci_get main.gitea_url)" || gitea_url="http://127.0.0.1:3000" gitea_user="$(uci_get main.gitea_user)" || gitea_user="admin" webhook_port="$(uci_get main.webhook_port)" || webhook_port="8088" webhook_secret="$(uci_get main.webhook_secret)" || webhook_secret="" # Content settings content_repo="$(uci_get content.repo_name)" || content_repo="blog-content" content_path="$(uci_get content.repo_path)" || content_path="/srv/metabolizer/content" github_mirror="$(uci_get content.github_mirror)" || github_mirror="" # CMS settings cms_repo="$(uci_get cms.repo_name)" || cms_repo="metabolizer-cms" cms_path="$(uci_get cms.repo_path)" || cms_path="/srv/metabolizer/cms" streamlit_app="$(uci_get cms.streamlit_app)" || streamlit_app="metabolizer" # Hexo settings hexo_source="$(uci_get hexo.source_path)" || hexo_source="/srv/hexojs/site/source/_posts" hexo_public="$(uci_get hexo.public_path)" || hexo_public="/srv/hexojs/site/public" portal_path="$(uci_get hexo.portal_path)" || portal_path="/www/blog" auto_publish="$(uci_get hexo.auto_publish)" || auto_publish="1" # Portal settings portal_enabled="$(uci_get portal.enabled)" || portal_enabled="1" portal_url="$(uci_get portal.url_path)" || portal_url="/blog" portal_title="$(uci_get portal.title)" || portal_title="SecuBox Blog" # Ensure directories ensure_dir "/srv/metabolizer" ensure_dir "$content_path" ensure_dir "$cms_path" } # Generate webhook secret generate_secret() { head -c 32 /dev/urandom | base64 | tr -d '\n/+=' } # Check if Gitea is running gitea_running() { lxc-info -n gitea -s 2>/dev/null | grep -q "RUNNING" } # Check if Streamlit is running streamlit_running() { lxc-info -n streamlit -s 2>/dev/null | grep -q "RUNNING" } # Check if HexoJS is running hexo_running() { lxc-info -n hexojs -s 2>/dev/null | grep -q "RUNNING" } # Usage usage() { cat < [options] Commands: install Setup repos in Gitea, deploy CMS to Streamlit uninstall Remove metabolizer setup (preserve content) status Show pipeline status (JSON) mirror Clone GitHub repo to local Gitea sync Pull latest from all repos build Trigger Hexo clean -> generate -> publish publish Copy static site to portal cms deploy Deploy CMS app to Streamlit cms update Pull and restart CMS webhook-listen Start webhook listener (used by init) webhook-handle Handle incoming webhook (internal) Configuration: /etc/config/metabolizer Data directories: /srv/metabolizer/content - Blog content repo /srv/metabolizer/cms - CMS app repo /www/blog - Static site output EOF } # Install metabolizer pipeline cmd_install() { require_root load_config log_info "Installing Metabolizer Blog Pipeline..." # Check prerequisites if ! gitea_running; then log_error "Gitea is not running. Start with: /etc/init.d/gitea start" return 1 fi if ! streamlit_running; then log_error "Streamlit is not running. Start with: /etc/init.d/streamlit start" return 1 fi # Generate webhook secret if not set if [ -z "$webhook_secret" ]; then webhook_secret=$(generate_secret) uci_set main.webhook_secret "$webhook_secret" log_info "Generated webhook secret" fi # Create content repo in Gitea (if not exists) log_info "Setting up content repository..." setup_gitea_repo "$content_repo" "Blog content repository" # Create CMS repo in Gitea (if not exists) log_info "Setting up CMS repository..." setup_gitea_repo "$cms_repo" "Metabolizer CMS Streamlit app" # Clone repos locally log_info "Cloning repositories..." clone_repo "$content_repo" "$content_path" clone_repo "$cms_repo" "$cms_path" # Deploy CMS to Streamlit log_info "Deploying CMS to Streamlit..." cmd_cms_deploy # Setup portal directory log_info "Setting up portal..." ensure_dir "$portal_path" # Enable service uci_set main.enabled '1' /etc/init.d/metabolizer enable 2>/dev/null || true log_info "Installation complete!" log_info "" log_info "Access CMS: http://:8501" log_info "View Blog: http://$portal_url" } # Setup Gitea repository setup_gitea_repo() { local repo_name="$1" local description="$2" # Check if repo exists via Gitea API local api_url="${gitea_url}/api/v1/repos/${gitea_user}/${repo_name}" # Try to get repo info local response=$(wget -q -O - "$api_url" 2>/dev/null) if echo "$response" | grep -q "\"name\":\"$repo_name\""; then log_info "Repository '$repo_name' already exists" return 0 fi # Create repo via Gitea CLI in container log_info "Creating repository '$repo_name'..." lxc-attach -n gitea -- su-exec git /usr/local/bin/gitea admin repo-create \ --username "$gitea_user" \ --name "$repo_name" \ --private false \ --config /data/custom/conf/app.ini 2>/dev/null || { log_error "Failed to create repository '$repo_name'" return 1 } log_info "Repository '$repo_name' created" } # Clone repository locally clone_repo() { local repo_name="$1" local local_path="$2" if [ -d "$local_path/.git" ]; then log_info "Repository '$repo_name' already cloned at $local_path" return 0 fi ensure_dir "$(dirname "$local_path")" local clone_url="${gitea_url}/${gitea_user}/${repo_name}.git" git clone "$clone_url" "$local_path" 2>/dev/null || { # If empty repo, init locally log_info "Initializing empty repository..." mkdir -p "$local_path" cd "$local_path" git init git remote add origin "$clone_url" } log_info "Repository '$repo_name' ready at $local_path" } # Mirror GitHub repo to Gitea cmd_mirror() { require_root load_config local github_url="$1" if [ -z "$github_url" ]; then log_error "Usage: metabolizerctl mirror " return 1 fi if ! gitea_running; then log_error "Gitea is not running" return 1 fi # Extract repo name from URL local repo_name=$(basename "$github_url" .git) log_info "Mirroring $github_url to local Gitea..." # Clone from GitHub local tmp_path="/tmp/metabolizer-mirror-$$" git clone --bare "$github_url" "$tmp_path" || { log_error "Failed to clone from GitHub" return 1 } # Create repo in Gitea setup_gitea_repo "$repo_name" "Mirrored from $github_url" # Push to local Gitea cd "$tmp_path" git remote add gitea "${gitea_url}/${gitea_user}/${repo_name}.git" git push --mirror gitea || { log_error "Failed to push to Gitea" rm -rf "$tmp_path" return 1 } rm -rf "$tmp_path" # Save mirror URL for future syncs uci_set content.github_mirror "$github_url" log_info "Mirror complete: $repo_name" echo "${gitea_url}/${gitea_user}/${repo_name}.git" } # Sync all repos cmd_sync() { require_root load_config log_info "Syncing repositories..." # Sync content repo if [ -d "$content_path/.git" ]; then log_info "Pulling content repo..." cd "$content_path" && git pull origin main 2>/dev/null || git pull origin master 2>/dev/null || true fi # Sync CMS repo if [ -d "$cms_path/.git" ]; then log_info "Pulling CMS repo..." cd "$cms_path" && git pull origin main 2>/dev/null || git pull origin master 2>/dev/null || true fi # Sync from GitHub mirror if configured if [ -n "$github_mirror" ] && [ -d "$content_path/.git" ]; then log_info "Syncing from GitHub mirror..." cd "$content_path" git fetch origin git reset --hard origin/main 2>/dev/null || git reset --hard origin/master 2>/dev/null || true fi log_info "Sync complete" } # Build static site with Hexo cmd_build() { require_root load_config log_info "Building static site..." # Sync content to Hexo source if [ -d "$content_path/_posts" ]; then log_info "Syncing posts to Hexo..." rsync -av --delete "$content_path/_posts/" "$hexo_source/" || true fi # Sync images if exists if [ -d "$content_path/images" ]; then log_info "Syncing images..." ensure_dir "/srv/hexojs/site/source/images" rsync -av "$content_path/images/" "/srv/hexojs/site/source/images/" || true fi # Run Hexo build if command -v hexoctl >/dev/null 2>&1; then log_info "Running Hexo clean..." hexoctl clean 2>/dev/null || true log_info "Running Hexo generate..." hexoctl generate || { log_error "Hexo build failed" return 1 } else log_error "hexoctl not found" return 1 fi # Auto-publish if enabled if [ "$auto_publish" = "1" ]; then cmd_publish fi log_info "Build complete" } # Publish to portal cmd_publish() { require_root load_config log_info "Publishing to portal..." if [ ! -d "$hexo_public" ]; then log_error "Hexo public directory not found. Run 'metabolizerctl build' first." return 1 fi ensure_dir "$portal_path" rsync -av --delete "$hexo_public/" "$portal_path/" || { log_error "Failed to publish to portal" return 1 } log_info "Published to $portal_path" } # Deploy CMS to Streamlit cmd_cms_deploy() { require_root load_config log_info "Deploying CMS to Streamlit..." local streamlit_apps="/srv/streamlit/apps" ensure_dir "$streamlit_apps" # Copy CMS app from package or repo if [ -d "$cms_path" ] && [ -f "$cms_path/app.py" ]; then # Use repo version cp -r "$cms_path"/* "$streamlit_apps/metabolizer/" 2>/dev/null || { mkdir -p "$streamlit_apps/metabolizer" cp -r "$cms_path"/* "$streamlit_apps/metabolizer/" } else # Use package default if [ -d "/usr/share/metabolizer/cms" ]; then mkdir -p "$streamlit_apps/metabolizer" cp -r /usr/share/metabolizer/cms/* "$streamlit_apps/metabolizer/" fi fi # Create symlink for Streamlit ln -sf "$streamlit_apps/metabolizer/app.py" "$streamlit_apps/metabolizer.py" 2>/dev/null || true # Set as active app uci set streamlit.main.active_app='metabolizer' uci commit streamlit # Restart Streamlit if running if streamlit_running; then /etc/init.d/streamlit restart 2>/dev/null & fi log_info "CMS deployed" } # Update CMS from repo cmd_cms_update() { require_root load_config log_info "Updating CMS..." # Pull latest if [ -d "$cms_path/.git" ]; then cd "$cms_path" && git pull origin main 2>/dev/null || git pull origin master 2>/dev/null || true fi # Redeploy cmd_cms_deploy log_info "CMS updated" } # Status cmd_status() { load_config local gitea_status="stopped" local streamlit_status="stopped" local hexo_status="stopped" gitea_running && gitea_status="running" streamlit_running && streamlit_status="running" hexo_running && hexo_status="running" # Count posts local post_count=0 if [ -d "$content_path/_posts" ]; then post_count=$(ls -1 "$content_path/_posts"/*.md 2>/dev/null | wc -l) fi # Get LAN IP local lan_ip lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1") cat << EOF { "enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"), "gitea": { "status": "$gitea_status", "url": "$gitea_url" }, "streamlit": { "status": "$streamlit_status", "app": "$streamlit_app" }, "hexo": { "status": "$hexo_status" }, "content": { "repo": "$content_repo", "path": "$content_path", "post_count": $post_count }, "portal": { "enabled": $([ "$portal_enabled" = "1" ] && echo "true" || echo "false"), "url": "http://${lan_ip}${portal_url}", "path": "$portal_path" }, "cms_url": "http://${lan_ip}:8501" } EOF } # Webhook listener cmd_webhook_listen() { load_config log_info "Starting webhook listener on port $webhook_port..." # Simple HTTP server using netcat while true; do echo -e "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{\"status\":\"ok\"}" | \ nc -l -p "$webhook_port" -q 1 | while read -r line; do # Check for POST data if echo "$line" | grep -q "^{"; then echo "$line" | /usr/bin/metabolizer-webhook & fi done sleep 1 done } # Uninstall cmd_uninstall() { require_root load_config log_info "Uninstalling Metabolizer..." # Disable service /etc/init.d/metabolizer stop 2>/dev/null || true /etc/init.d/metabolizer disable 2>/dev/null || true uci_set main.enabled '0' # Remove Streamlit app link (keep data) rm -f /srv/streamlit/apps/metabolizer.py 2>/dev/null || true log_info "Metabolizer disabled" log_info "Content preserved in: $content_path" } # Main case "${1:-}" in install) shift; cmd_install "$@" ;; uninstall) shift; cmd_uninstall "$@" ;; status) shift; cmd_status "$@" ;; mirror) shift; cmd_mirror "$@" ;; sync) shift; cmd_sync "$@" ;; build) shift; cmd_build "$@" ;; publish) shift; cmd_publish "$@" ;; cms) shift case "${1:-}" in deploy) shift; cmd_cms_deploy "$@" ;; update) shift; cmd_cms_update "$@" ;; *) echo "Usage: metabolizerctl cms {deploy|update}"; exit 1 ;; esac ;; webhook-listen) shift; cmd_webhook_listen "$@" ;; *) usage ;; esac