secubox-openwrt/package/secubox/secubox-app-droplet/files/usr/sbin/dropletctl
CyberMind-FR 078e2dc01e feat(droplet): Add one-drop content publisher
Simple drag-and-drop publishing for HTML/ZIP files:
- Auto-detects content type (static/streamlit/hexo)
- Creates vhosts at gk2.secubox.in by default
- Registers with metablogizer or streamlit accordingly
- CLI: dropletctl publish/list/remove/rename
- LuCI drag-drop interface at Services > Droplet

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

293 lines
12 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"
# Extract if ZIP
local content_type=$(file -b --mime-type "$file")
if echo "$content_type" | grep -q "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 echo "$content_type" | grep -qE "html|text"; then
# Single HTML file
cp "$file" "$tmp_dir/index.html"
else
log_error "Unsupported file type: $content_type"
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/"
# Create vhost via haproxyctl
log_info "Creating vhost: $vhost"
if command -v haproxyctl >/dev/null 2>&1; then
haproxyctl vhost add "$vhost" 2>/dev/null || true
fi
# Register with appropriate system
if [ "$publish_method" = "streamlit" ]; then
# Add to streamlit config
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
uci set "metablogizer.${name}=site"
uci set "metablogizer.${name}.name=$name"
uci set "metablogizer.${name}.domain=$vhost"
uci set "metablogizer.${name}.enabled=1"
uci commit metablogizer
log_info "Registered MetaBlog site"
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
uci show metablogizer 2>/dev/null | grep "=site$" | sed "s/metablogizer\.\(.*\)=site/\1/" | while read name; do
domain=$(uci -q get "metablogizer.$name.domain")
enabled=$(uci -q get "metablogizer.$name.enabled")
[ "$enabled" = "1" ] && status="[ON]" || status="[OFF]"
printf "%-30s %s %s\n" "$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; }
# Check metablogizer
if uci -q get "metablogizer.$name" >/dev/null 2>&1; then
uci delete "metablogizer.$name"
uci commit metablogizer
rm -rf "$SITES_DIR/$name"
log_ok "Removed MetaBlog: $name"
fi
# Check streamlit
if uci -q get "streamlit.$name" >/dev/null 2>&1; then
uci delete "streamlit.$name"
uci commit streamlit
rm -rf "$APPS_DIR/$name"
log_ok "Removed Streamlit: $name"
fi
# Remove vhost
if command -v haproxyctl >/dev/null 2>&1; then
haproxyctl vhost remove "${name}.${DEFAULT_DOMAIN}" 2>/dev/null || true
fi
/etc/init.d/haproxy 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