P2P App Store Emancipation: - secubox-p2p: Package distribution via mesh peers (CGI API, RPCD, CLI) - packages.js: LuCI view with LOCAL/PEER badges, fetch/install actions - devstatus.js: Dev Status widget with Gitea commits, v1.0 progress tracking - secubox-feed: sync-content command for auto-installing content packages - ACL fix for P2P feed RPCD methods Remote Access: - secubox-app-rustdesk: Native hbbs/hbbr relay server from GitHub releases - secubox-app-guacamole: LXC Debian container with guacd + Tomcat (partial) Content Distribution: - secubox-content-pkg: Auto-package Metablogizer/Streamlit as IPKs - Auto-publish hooks in metablogizerctl and streamlitctl Mesh Media: - secubox-app-ksmbd: In-kernel SMB3 server with ksmbdctl CLI - Pre-configured shares for Jellyfin, Lyrion, Backup UI Consistency: - client-guardian: Ported to sh-page-header chip layout - auth-guardian: Ported to sh-page-header chip layout Fixes: - services.js: RPC expect unwrapping bug fix - metablogizer: Chunked upload for uhttpd 64KB limit Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
418 lines
9.4 KiB
Bash
418 lines
9.4 KiB
Bash
#!/bin/sh
|
|
# SecuBox Content Packager
|
|
# Package Metablogizer sites and Streamlit apps as IPKs for P2P distribution
|
|
|
|
FEED_PATH="/www/secubox-feed"
|
|
SITES_PATH="/srv/metablogizer"
|
|
STREAMLIT_PATH="/srv/streamlit/apps"
|
|
TMP_DIR="/tmp/content-pkg"
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage: secubox-content-pkg <command> [args]
|
|
|
|
Commands:
|
|
site <name> [domain] Package Metablogizer site as IPK
|
|
streamlit <name> [port] Package Streamlit app as IPK
|
|
list List content packages in feed
|
|
remove <pkg-name> Remove content package from feed
|
|
rebuild-index Rebuild Packages index
|
|
USAGE
|
|
}
|
|
|
|
log_info() { echo "[INFO] $*"; logger -t content-pkg "$*"; }
|
|
log_error() { echo "[ERROR] $*" >&2; logger -t content-pkg -p err "$*"; }
|
|
|
|
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
|
|
|
|
# Get next available port for Streamlit
|
|
get_next_port() {
|
|
local base_port=8501
|
|
local port=$base_port
|
|
while [ $port -lt 8600 ]; do
|
|
if ! uci show streamlit 2>/dev/null | grep -q "port='$port'"; then
|
|
echo $port
|
|
return
|
|
fi
|
|
port=$((port + 1))
|
|
done
|
|
echo $base_port
|
|
}
|
|
|
|
# Build IPK from directory
|
|
build_ipk() {
|
|
local pkg_dir="$1"
|
|
local output_dir="$2"
|
|
local pkg_name=$(grep "^Package:" "$pkg_dir/CONTROL/control" | cut -d: -f2 | tr -d ' ')
|
|
local version=$(grep "^Version:" "$pkg_dir/CONTROL/control" | cut -d: -f2 | tr -d ' ')
|
|
local arch=$(grep "^Architecture:" "$pkg_dir/CONTROL/control" | cut -d: -f2 | tr -d ' ')
|
|
local ipk_name="${pkg_name}_${version}_${arch}.ipk"
|
|
|
|
cd "$pkg_dir" || return 1
|
|
|
|
# Create data.tar.gz (exclude CONTROL)
|
|
tar --exclude='CONTROL' -czf "$TMP_DIR/data.tar.gz" ./* 2>/dev/null
|
|
|
|
# Create control.tar.gz
|
|
tar -czf "$TMP_DIR/control.tar.gz" -C CONTROL . 2>/dev/null
|
|
|
|
# Create debian-binary
|
|
echo "2.0" > "$TMP_DIR/debian-binary"
|
|
|
|
# Create IPK (ar archive)
|
|
cd "$TMP_DIR"
|
|
tar -czf "$output_dir/$ipk_name" debian-binary control.tar.gz data.tar.gz 2>/dev/null || \
|
|
ar -r "$output_dir/$ipk_name" debian-binary control.tar.gz data.tar.gz 2>/dev/null
|
|
|
|
# Cleanup temp files
|
|
rm -f "$TMP_DIR/data.tar.gz" "$TMP_DIR/control.tar.gz" "$TMP_DIR/debian-binary"
|
|
|
|
echo "$ipk_name"
|
|
}
|
|
|
|
# Rebuild Packages index
|
|
rebuild_index() {
|
|
cd "$FEED_PATH" || return 1
|
|
|
|
log_info "Rebuilding Packages index..."
|
|
|
|
# Generate Packages file
|
|
> Packages
|
|
for ipk in *.ipk; do
|
|
[ -f "$ipk" ] || continue
|
|
|
|
# Extract control info
|
|
local tmpctl="/tmp/ctl-$$"
|
|
mkdir -p "$tmpctl"
|
|
|
|
# Try tar first (our format), then ar (standard opkg)
|
|
tar -xzf "$ipk" -C "$tmpctl" control.tar.gz 2>/dev/null && \
|
|
tar -xzf "$tmpctl/control.tar.gz" -C "$tmpctl" 2>/dev/null
|
|
|
|
if [ ! -f "$tmpctl/control" ]; then
|
|
# Standard ar format
|
|
cd "$tmpctl"
|
|
ar -x "../$ipk" control.tar.gz 2>/dev/null
|
|
tar -xzf control.tar.gz 2>/dev/null
|
|
cd "$FEED_PATH"
|
|
fi
|
|
|
|
if [ -f "$tmpctl/control" ]; then
|
|
cat "$tmpctl/control" >> Packages
|
|
echo "Filename: $ipk" >> Packages
|
|
echo "Size: $(stat -c%s "$ipk" 2>/dev/null || wc -c < "$ipk")" >> Packages
|
|
echo "SHA256sum: $(sha256sum "$ipk" | cut -d' ' -f1)" >> Packages
|
|
echo "" >> Packages
|
|
fi
|
|
|
|
rm -rf "$tmpctl"
|
|
done
|
|
|
|
# Compress
|
|
gzip -kf Packages 2>/dev/null
|
|
|
|
log_info "Index rebuilt: $(grep -c "^Package:" Packages) packages"
|
|
}
|
|
|
|
# ---------- Site Packaging ----------
|
|
|
|
pkg_site() {
|
|
local name="$1"
|
|
local domain="${2:-${name}.secubox.local}"
|
|
local site_path="$SITES_PATH/$name/public"
|
|
local version="1.0.0-$(date +%Y%m%d%H%M)"
|
|
local pkg_name="secubox-site-${name}"
|
|
local pkg_dir="$TMP_DIR/$pkg_name"
|
|
|
|
# Validate
|
|
if [ ! -d "$site_path" ]; then
|
|
log_error "Site not found: $site_path"
|
|
log_error "Run 'metablogizerctl build $name' first"
|
|
return 1
|
|
fi
|
|
|
|
log_info "Packaging site: $name"
|
|
|
|
# Create package structure
|
|
ensure_dir "$pkg_dir/www/sites/$name"
|
|
ensure_dir "$pkg_dir/CONTROL"
|
|
ensure_dir "$FEED_PATH"
|
|
|
|
# Copy site files
|
|
cp -a "$site_path"/* "$pkg_dir/www/sites/$name/" || {
|
|
log_error "Failed to copy site files"
|
|
return 1
|
|
}
|
|
|
|
# Calculate installed size
|
|
local installed_size=$(du -sk "$pkg_dir/www" | cut -f1)
|
|
|
|
# Create control file
|
|
cat > "$pkg_dir/CONTROL/control" <<EOF
|
|
Package: $pkg_name
|
|
Version: $version
|
|
Architecture: all
|
|
Installed-Size: $installed_size
|
|
Description: Metablogizer site: $name
|
|
Auto-generated content package for P2P distribution.
|
|
Domain: $domain
|
|
Maintainer: SecuBox Content Packager
|
|
Section: www
|
|
Priority: optional
|
|
Source: secubox-content-pkg
|
|
EOF
|
|
|
|
# Create postinst
|
|
cat > "$pkg_dir/CONTROL/postinst" <<EOF
|
|
#!/bin/sh
|
|
# Auto-configure HAProxy vhost for site
|
|
SITE_NAME="$name"
|
|
SITE_DOMAIN="$domain"
|
|
|
|
if command -v haproxyctl >/dev/null 2>&1; then
|
|
# Add vhost pointing to uhttpd
|
|
haproxyctl add-vhost "\$SITE_DOMAIN" "127.0.0.1:80" "/sites/\$SITE_NAME" 2>/dev/null || true
|
|
fi
|
|
|
|
# Log installation
|
|
logger -t content-pkg "Installed site: \$SITE_NAME at /www/sites/\$SITE_NAME"
|
|
|
|
exit 0
|
|
EOF
|
|
chmod +x "$pkg_dir/CONTROL/postinst"
|
|
|
|
# Create prerm
|
|
cat > "$pkg_dir/CONTROL/prerm" <<EOF
|
|
#!/bin/sh
|
|
SITE_NAME="$name"
|
|
SITE_DOMAIN="$domain"
|
|
|
|
if command -v haproxyctl >/dev/null 2>&1; then
|
|
haproxyctl remove-vhost "\$SITE_DOMAIN" 2>/dev/null || true
|
|
fi
|
|
|
|
exit 0
|
|
EOF
|
|
chmod +x "$pkg_dir/CONTROL/prerm"
|
|
|
|
# Build IPK
|
|
local ipk_file=$(build_ipk "$pkg_dir" "$FEED_PATH")
|
|
|
|
# Cleanup
|
|
rm -rf "$pkg_dir"
|
|
|
|
if [ -n "$ipk_file" ] && [ -f "$FEED_PATH/$ipk_file" ]; then
|
|
log_info "Created: $FEED_PATH/$ipk_file"
|
|
rebuild_index
|
|
return 0
|
|
else
|
|
log_error "Failed to create IPK"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# ---------- Streamlit Packaging ----------
|
|
|
|
pkg_streamlit() {
|
|
local name="$1"
|
|
local port="${2:-$(get_next_port)}"
|
|
local app_path="$STREAMLIT_PATH/$name"
|
|
local version="1.0.0-$(date +%Y%m%d%H%M)"
|
|
local pkg_name="secubox-streamlit-${name}"
|
|
local pkg_dir="$TMP_DIR/$pkg_name"
|
|
|
|
# Validate
|
|
if [ ! -d "$app_path" ]; then
|
|
log_error "Streamlit app not found: $app_path"
|
|
return 1
|
|
fi
|
|
|
|
# Find main app file
|
|
local app_file=""
|
|
for f in "$app_path/app.py" "$app_path/main.py" "$app_path"/*.py; do
|
|
if [ -f "$f" ]; then
|
|
app_file=$(basename "$f")
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ -z "$app_file" ]; then
|
|
log_error "No Python file found in $app_path"
|
|
return 1
|
|
fi
|
|
|
|
log_info "Packaging Streamlit app: $name (port $port)"
|
|
|
|
# Create package structure
|
|
ensure_dir "$pkg_dir/srv/streamlit/apps/$name"
|
|
ensure_dir "$pkg_dir/CONTROL"
|
|
ensure_dir "$FEED_PATH"
|
|
|
|
# Copy app files
|
|
cp -a "$app_path"/* "$pkg_dir/srv/streamlit/apps/$name/" || {
|
|
log_error "Failed to copy app files"
|
|
return 1
|
|
}
|
|
|
|
# Calculate installed size
|
|
local installed_size=$(du -sk "$pkg_dir/srv" | cut -f1)
|
|
|
|
# Create control file
|
|
cat > "$pkg_dir/CONTROL/control" <<EOF
|
|
Package: $pkg_name
|
|
Version: $version
|
|
Architecture: all
|
|
Installed-Size: $installed_size
|
|
Depends: secubox-app-streamlit
|
|
Description: Streamlit app: $name
|
|
Auto-generated content package for P2P distribution.
|
|
Port: $port
|
|
Maintainer: SecuBox Content Packager
|
|
Section: utils
|
|
Priority: optional
|
|
Source: secubox-content-pkg
|
|
EOF
|
|
|
|
# Create postinst
|
|
cat > "$pkg_dir/CONTROL/postinst" <<EOF
|
|
#!/bin/sh
|
|
# Register Streamlit instance
|
|
APP_NAME="$name"
|
|
APP_PORT="$port"
|
|
APP_FILE="$app_file"
|
|
|
|
# Add to UCI
|
|
uci set streamlit.\${APP_NAME}=instance
|
|
uci set streamlit.\${APP_NAME}.enabled='1'
|
|
uci set streamlit.\${APP_NAME}.app="\$APP_NAME"
|
|
uci set streamlit.\${APP_NAME}.port="\$APP_PORT"
|
|
uci set streamlit.\${APP_NAME}.script="\$APP_FILE"
|
|
uci commit streamlit
|
|
|
|
# Restart Streamlit to pick up new instance
|
|
if [ -x /etc/init.d/streamlit ]; then
|
|
/etc/init.d/streamlit reload 2>/dev/null || /etc/init.d/streamlit restart 2>/dev/null
|
|
fi
|
|
|
|
# Log
|
|
logger -t content-pkg "Installed Streamlit app: \$APP_NAME on port \$APP_PORT"
|
|
|
|
exit 0
|
|
EOF
|
|
chmod +x "$pkg_dir/CONTROL/postinst"
|
|
|
|
# Create prerm
|
|
cat > "$pkg_dir/CONTROL/prerm" <<EOF
|
|
#!/bin/sh
|
|
APP_NAME="$name"
|
|
|
|
# Remove from UCI
|
|
uci delete streamlit.\${APP_NAME} 2>/dev/null
|
|
uci commit streamlit
|
|
|
|
# Restart Streamlit
|
|
if [ -x /etc/init.d/streamlit ]; then
|
|
/etc/init.d/streamlit reload 2>/dev/null
|
|
fi
|
|
|
|
exit 0
|
|
EOF
|
|
chmod +x "$pkg_dir/CONTROL/prerm"
|
|
|
|
# Build IPK
|
|
local ipk_file=$(build_ipk "$pkg_dir" "$FEED_PATH")
|
|
|
|
# Cleanup
|
|
rm -rf "$pkg_dir"
|
|
|
|
if [ -n "$ipk_file" ] && [ -f "$FEED_PATH/$ipk_file" ]; then
|
|
log_info "Created: $FEED_PATH/$ipk_file"
|
|
rebuild_index
|
|
return 0
|
|
else
|
|
log_error "Failed to create IPK"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# ---------- List/Remove ----------
|
|
|
|
cmd_list() {
|
|
echo "Content Packages in Feed:"
|
|
echo "========================="
|
|
|
|
cd "$FEED_PATH" 2>/dev/null || { echo "Feed not found"; return 1; }
|
|
|
|
local found=0
|
|
for ipk in secubox-site-*.ipk secubox-streamlit-*.ipk; do
|
|
[ -f "$ipk" ] || continue
|
|
found=1
|
|
|
|
local name=$(echo "$ipk" | sed 's/_[0-9].*$//')
|
|
local size=$(ls -lh "$ipk" | awk '{print $5}')
|
|
|
|
if echo "$ipk" | grep -q "secubox-site-"; then
|
|
printf " [SITE] %-30s %s\n" "$name" "$size"
|
|
else
|
|
printf " [STREAMLIT] %-30s %s\n" "$name" "$size"
|
|
fi
|
|
done
|
|
|
|
[ "$found" = "0" ] && echo " (no content packages)"
|
|
}
|
|
|
|
cmd_remove() {
|
|
local pkg_name="$1"
|
|
|
|
[ -z "$pkg_name" ] && { echo "Usage: secubox-content-pkg remove <pkg-name>"; return 1; }
|
|
|
|
cd "$FEED_PATH" 2>/dev/null || { log_error "Feed not found"; return 1; }
|
|
|
|
# Find matching IPK
|
|
local found=""
|
|
for ipk in "${pkg_name}"*.ipk "${pkg_name}_"*.ipk; do
|
|
if [ -f "$ipk" ]; then
|
|
found="$ipk"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ -z "$found" ]; then
|
|
log_error "Package not found: $pkg_name"
|
|
return 1
|
|
fi
|
|
|
|
rm -f "$found"
|
|
log_info "Removed: $found"
|
|
rebuild_index
|
|
}
|
|
|
|
# ---------- Main ----------
|
|
|
|
ensure_dir "$TMP_DIR"
|
|
|
|
case "$1" in
|
|
site)
|
|
shift
|
|
pkg_site "$@"
|
|
;;
|
|
streamlit)
|
|
shift
|
|
pkg_streamlit "$@"
|
|
;;
|
|
list)
|
|
cmd_list
|
|
;;
|
|
remove)
|
|
shift
|
|
cmd_remove "$@"
|
|
;;
|
|
rebuild-index)
|
|
rebuild_index
|
|
;;
|
|
*)
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|