secubox-openwrt/package/secubox/secubox-app-droplet/files/usr/sbin/dropletctl
CyberMind-FR 078a3bea5f fix(droplet): Proper metablogizer integration and permissions
- Fix file permissions (chmod 644/755) after upload
- Use site_${name} UCI section naming for metablogizer
- Auto-assign port and call metablogizerctl publish
- Generate README.nfo for new droplets
- Handle both old/new section naming in list/remove

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-14 11:43:24 +01:00

348 lines
14 KiB
Bash

#!/bin/sh
# ═══════════════════════════════════════════════════════════════════════════════
# Droplet Publisher - One-Drop Content Publishing
# Drop HTML/ZIP → Get published site with vhost + Gitea versioning
# ═══════════════════════════════════════════════════════════════════════════════
DROPLET_DIR="/srv/droplet"
SITES_DIR="/srv/metablogizer/sites"
APPS_DIR="/srv/streamlit/apps"
DEFAULT_DOMAIN="gk2.secubox.in"
GITEA_REPO="gandalf/droplet-sites"
GITEA_URL="https://git.gk2.secubox.in"
# Logging
log_info() { logger -t droplet -p user.info "$*"; echo "[INFO] $*"; }
log_error() { logger -t droplet -p user.error "$*"; echo "[ERROR] $*" >&2; }
log_ok() { echo "[OK] $*"; }
# ─────────────────────────────────────────────────────────────────────────────────
# Detect content type from file/directory
# Returns: static|streamlit|hexo|unknown
# ─────────────────────────────────────────────────────────────────────────────────
detect_type() {
local path="$1"
# Check for Streamlit app
if [ -f "$path/app.py" ] || [ -f "$path/main.py" ]; then
grep -qE "import streamlit|from streamlit" "$path"/*.py 2>/dev/null && {
echo "streamlit"
return
}
fi
# Check for Hexo
if [ -f "$path/_config.yml" ] && [ -d "$path/source" ]; then
echo "hexo"
return
fi
# Check for static HTML
if [ -f "$path/index.html" ] || [ -f "$path/index.htm" ]; then
echo "static"
return
fi
# Single HTML file
if [ -f "$path" ] && echo "$path" | grep -qiE '\.html?$'; then
echo "static"
return
fi
echo "unknown"
}
# ─────────────────────────────────────────────────────────────────────────────────
# Publish content
# Usage: dropletctl publish <file> <name> [domain]
# ─────────────────────────────────────────────────────────────────────────────────
cmd_publish() {
local file="$1"
local name="$2"
local domain="${3:-$DEFAULT_DOMAIN}"
[ -z "$file" ] && { log_error "Usage: dropletctl publish <file> <name> [domain]"; return 1; }
[ -z "$name" ] && { log_error "Name required"; return 1; }
[ ! -f "$file" ] && { log_error "File not found: $file"; return 1; }
# Sanitize name
name=$(echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/_/g')
local vhost="${name}.${domain}"
local tmp_dir="/tmp/droplet_$$"
mkdir -p "$tmp_dir"
log_info "Publishing: $file as $vhost"
# Detect file type by extension (file command not available on OpenWrt)
local file_ext=$(echo "$file" | sed 's/.*\.//' | tr '[:upper:]' '[:lower:]')
if [ "$file_ext" = "zip" ]; then
log_info "Extracting ZIP..."
unzip -q "$file" -d "$tmp_dir" || { log_error "Failed to extract ZIP"; rm -rf "$tmp_dir"; return 1; }
# Handle nested directory
local nested=$(find "$tmp_dir" -mindepth 1 -maxdepth 1 -type d | head -1)
if [ -n "$nested" ] && [ $(find "$tmp_dir" -mindepth 1 -maxdepth 1 | wc -l) -eq 1 ]; then
mv "$nested"/* "$tmp_dir/" 2>/dev/null
rmdir "$nested" 2>/dev/null
fi
elif [ "$file_ext" = "html" ] || [ "$file_ext" = "htm" ]; then
# Single HTML file
cp "$file" "$tmp_dir/index.html"
else
log_error "Unsupported file type: .$file_ext (expected .html, .htm, or .zip)"
rm -rf "$tmp_dir"
return 1
fi
# Detect content type
local app_type=$(detect_type "$tmp_dir")
log_info "Detected type: $app_type"
local target_dir=""
local publish_method=""
case "$app_type" in
streamlit)
target_dir="$APPS_DIR/$name"
publish_method="streamlit"
;;
static|hexo)
target_dir="$SITES_DIR/$name"
publish_method="metablog"
;;
*)
# Default to static site
target_dir="$SITES_DIR/$name"
publish_method="metablog"
;;
esac
# Deploy content
log_info "Deploying to $target_dir..."
mkdir -p "$target_dir"
cp -r "$tmp_dir"/* "$target_dir/"
# Fix permissions (cgi-io uploads with 600)
find "$target_dir" -type f -exec chmod 644 {} \;
find "$target_dir" -type d -exec chmod 755 {} \;
# Generate README.nfo if not present
if [ ! -f "$target_dir/README.nfo" ]; then
log_info "Generating README.nfo..."
cat > "$target_dir/README.nfo" <<NFOEOF
[identity]
name=$name
id=droplet-$name
version=1.0.0
type=$app_type
[description]
short=Droplet-published $app_type site
long=Site published via Droplet one-drop publisher
[tags]
category=$app_type
keywords=droplet, published
audience=general
[runtime]
port=auto
NFOEOF
fi
# Register with appropriate system
if [ "$publish_method" = "streamlit" ]; then
# Add to streamlit config with proper naming
local port=$(uci show streamlit 2>/dev/null | grep -oE "port='[0-9]+'" | grep -oE "[0-9]+" | sort -n | tail -1)
port=$((${port:-8500} + 1))
uci set "streamlit.${name}=instance"
uci set "streamlit.${name}.name=$name"
uci set "streamlit.${name}.domain=$vhost"
uci set "streamlit.${name}.port=$port"
uci set "streamlit.${name}.enabled=1"
uci commit streamlit
log_info "Registered Streamlit app on port $port"
else
# Add to metablogizer config with proper site_ prefix and port
local port=$(uci show metablogizer 2>/dev/null | grep -oE "port='[0-9]+'" | grep -oE "[0-9]+" | sort -n | tail -1)
port=$((${port:-8949} + 1))
uci set "metablogizer.site_${name}=site"
uci set "metablogizer.site_${name}.name=$name"
uci set "metablogizer.site_${name}.domain=$vhost"
uci set "metablogizer.site_${name}.port=$port"
uci set "metablogizer.site_${name}.enabled=1"
uci commit metablogizer
log_info "Registered MetaBlog site on port $port"
# Use metablogizerctl to fully publish (creates uhttpd, HAProxy, mitmproxy routes)
if command -v metablogizerctl >/dev/null 2>&1; then
log_info "Running metablogizerctl publish..."
metablogizerctl publish "$name" 2>&1 | grep -E "^\[" || true
fi
fi
# Create vhost via haproxyctl (fallback if metablogizerctl not available)
if [ "$publish_method" = "streamlit" ]; then
log_info "Creating vhost: $vhost"
if command -v haproxyctl >/dev/null 2>&1; then
haproxyctl vhost add "$vhost" 2>/dev/null || true
fi
fi
# Git commit if available
if [ -d "$target_dir/.git" ] || command -v git >/dev/null 2>&1; then
cd "$target_dir"
if [ ! -d ".git" ]; then
git init -q
git remote add origin "${GITEA_URL}/${GITEA_REPO}/${name}.git" 2>/dev/null || true
fi
git add -A
git commit -q -m "Droplet publish: $name" 2>/dev/null || true
log_info "Committed to git"
fi
# Reload HAProxy
/etc/init.d/haproxy reload 2>/dev/null || true
# Cleanup
rm -rf "$tmp_dir"
log_ok "Published: https://$vhost/"
echo "$vhost"
}
# ─────────────────────────────────────────────────────────────────────────────────
# List published droplets
# ─────────────────────────────────────────────────────────────────────────────────
cmd_list() {
echo "=== Published Droplets ==="
# MetaBlog sites (handles both site_xxx and xxx section names)
uci show metablogizer 2>/dev/null | grep "=site$" | sed "s/metablogizer\.\(.*\)=site/\1/" | while read section; do
# Extract display name (remove site_ prefix if present)
display_name=$(echo "$section" | sed 's/^site_//')
domain=$(uci -q get "metablogizer.$section.domain")
enabled=$(uci -q get "metablogizer.$section.enabled")
[ "$enabled" = "1" ] && status="[ON]" || status="[OFF]"
printf "%-30s %s %s\n" "$display_name" "$status" "https://$domain/"
done
# Streamlit apps
uci show streamlit 2>/dev/null | grep "=instance$" | sed "s/streamlit\.\(.*\)=instance/\1/" | while read name; do
domain=$(uci -q get "streamlit.$name.domain")
enabled=$(uci -q get "streamlit.$name.enabled")
[ "$enabled" = "1" ] && status="[ON]" || status="[OFF]"
printf "%-30s %s %s (streamlit)\n" "$name" "$status" "https://$domain/"
done
}
# ─────────────────────────────────────────────────────────────────────────────────
# Remove a droplet
# ─────────────────────────────────────────────────────────────────────────────────
cmd_remove() {
local name="$1"
[ -z "$name" ] && { log_error "Usage: dropletctl remove <name>"; return 1; }
local found=0
# Check metablogizer (try both site_xxx and xxx section names)
for section in "site_$name" "$name"; do
if uci -q get "metablogizer.$section" >/dev/null 2>&1; then
local domain=$(uci -q get "metablogizer.$section.domain")
uci delete "metablogizer.$section"
uci commit metablogizer
rm -rf "$SITES_DIR/$name"
# Also remove uhttpd instance
uci -q delete "uhttpd.metablog_$name" 2>/dev/null
uci commit uhttpd 2>/dev/null || true
log_ok "Removed MetaBlog: $name"
found=1
# Remove vhost
[ -n "$domain" ] && haproxyctl vhost remove "$domain" 2>/dev/null || true
break
fi
done
# Check streamlit
if uci -q get "streamlit.$name" >/dev/null 2>&1; then
local domain=$(uci -q get "streamlit.$name.domain")
uci delete "streamlit.$name"
uci commit streamlit
rm -rf "$APPS_DIR/$name"
log_ok "Removed Streamlit: $name"
found=1
[ -n "$domain" ] && haproxyctl vhost remove "$domain" 2>/dev/null || true
fi
[ "$found" = "0" ] && log_error "Droplet '$name' not found"
haproxyctl reload 2>/dev/null || true
}
# ─────────────────────────────────────────────────────────────────────────────────
# Rename a droplet
# ─────────────────────────────────────────────────────────────────────────────────
cmd_rename() {
local old="$1"
local new="$2"
[ -z "$old" ] || [ -z "$new" ] && { log_error "Usage: dropletctl rename <old> <new>"; return 1; }
new=$(echo "$new" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/_/g')
# Check metablogizer
if uci -q get "metablogizer.$old" >/dev/null 2>&1; then
local domain="${new}.${DEFAULT_DOMAIN}"
mv "$SITES_DIR/$old" "$SITES_DIR/$new" 2>/dev/null
uci rename "metablogizer.$old=$new"
uci set "metablogizer.$new.name=$new"
uci set "metablogizer.$new.domain=$domain"
uci commit metablogizer
log_ok "Renamed MetaBlog: $old -> $new"
fi
# Check streamlit
if uci -q get "streamlit.$old" >/dev/null 2>&1; then
local domain="${new}.${DEFAULT_DOMAIN}"
mv "$APPS_DIR/$old" "$APPS_DIR/$new" 2>/dev/null
uci rename "streamlit.$old=$new"
uci set "streamlit.$new.name=$new"
uci set "streamlit.$new.domain=$domain"
uci commit streamlit
log_ok "Renamed Streamlit: $old -> $new"
fi
/etc/init.d/haproxy reload 2>/dev/null || true
}
# ─────────────────────────────────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────────────────────────────────
case "$1" in
publish) shift; cmd_publish "$@" ;;
list) cmd_list ;;
remove) shift; cmd_remove "$@" ;;
rename) shift; cmd_rename "$@" ;;
*)
echo "Droplet Publisher - One-Drop Content Publishing"
echo ""
echo "Usage: dropletctl <command> [args]"
echo ""
echo "Commands:"
echo " publish <file> <name> [domain] Publish HTML/ZIP as site"
echo " list List published droplets"
echo " remove <name> Remove a droplet"
echo " rename <old> <new> Rename a droplet"
echo ""
echo "Examples:"
echo " dropletctl publish mysite.zip mysite"
echo " dropletctl publish index.html landing"
echo " dropletctl rename landing homepage"
;;
esac