Metabolizer Blog Pipeline - integrated CMS for SecuBox: - Gitea: Mirror GitHub repos, store blog content - Streamlit: CMS app with markdown editor and live preview - HexoJS: Static site generator (clean → generate → publish) - Webhooks: Auto-rebuild on git push - Portal: Static blog served at /blog/ Pipeline: Edit in Streamlit CMS → Push to Gitea → Build with Hexo → Publish Packages: - secubox-app-streamlit: Streamlit server with LXC container - luci-app-streamlit: LuCI dashboard for Streamlit apps - secubox-app-metabolizer: CMS pipeline orchestrator CMS Features: - Two-column markdown editor with live preview - YAML front matter editor - Post management (drafts, publish, unpublish) - Media library with image upload - Git sync and Hexo build controls - Cyberpunk theme styling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
539 lines
13 KiB
Bash
539 lines
13 KiB
Bash
#!/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 <<EOF
|
|
SecuBox Metabolizer Blog Pipeline Controller
|
|
|
|
Usage: $(basename $0) <command> [options]
|
|
|
|
Commands:
|
|
install Setup repos in Gitea, deploy CMS to Streamlit
|
|
uninstall Remove metabolizer setup (preserve content)
|
|
status Show pipeline status (JSON)
|
|
|
|
mirror <url> 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://<router-ip>:8501"
|
|
log_info "View Blog: http://<router-ip>$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 <github-url>"
|
|
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
|