feat(repo): Add secubox-app-repo and luci-app-repo packages

Backend package (secubox-app-repo):
- repoctl CLI for managing local package repository
- repo-sync script to download packages from GitHub releases
- uhttpd-based server on port 8888
- UCI configuration at /etc/config/repo
- RPCD handler for LuCI integration
- Auto-sync cron support (configurable interval)

Frontend package (luci-app-repo):
- Dashboard showing repository status and package counts
- Sync button to trigger package downloads
- Log viewer for sync operations
- Usage instructions for opkg configuration

Supported architectures:
- x86_64, aarch64_cortex-a72, aarch64_generic
- mips_24kc, mipsel_24kc

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-03-18 10:21:23 +01:00
parent 276685f109
commit 9cd59b77ba
11 changed files with 798 additions and 0 deletions

View File

@ -0,0 +1,26 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-repo
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=SecuBox Team
PKG_LICENSE:=Apache-2.0
LUCI_TITLE:=LuCI Package Repository Dashboard
LUCI_DEPENDS:=+secubox-app-repo
include $(INCLUDE_DIR)/package.mk
define Package/luci-app-repo/install
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/repo
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/repo/*.js $(1)/www/luci-static/resources/view/repo/
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-repo.json $(1)/usr/share/luci/menu.d/
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-repo.json $(1)/usr/share/rpcd/acl.d/
endef
$(eval $(call BuildPackage,luci-app-repo))

View File

@ -0,0 +1,203 @@
'use strict';
'require view';
'require dom';
'require poll';
'require uci';
'require rpc';
'require ui';
var callStatus = rpc.declare({
object: 'luci.repo',
method: 'status',
expect: {}
});
var callPackages = rpc.declare({
object: 'luci.repo',
method: 'packages',
params: ['arch'],
expect: {}
});
var callSync = rpc.declare({
object: 'luci.repo',
method: 'sync',
params: ['version'],
expect: {}
});
var callLogs = rpc.declare({
object: 'luci.repo',
method: 'logs',
params: ['lines'],
expect: {}
});
return view.extend({
load: function() {
return Promise.all([
callStatus(),
uci.load('repo')
]);
},
formatBytes: function(bytes) {
if (bytes === 0) return '0 B';
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
},
renderStatus: function(status) {
var statusClass = status.running ? 'success' : 'danger';
var statusText = status.running ? _('Running') : _('Stopped');
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Repository Status')),
E('div', { 'class': 'table' }, [
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td left', 'style': 'width:200px' }, _('Server Status')),
E('div', { 'class': 'td' }, [
E('span', { 'class': 'label ' + statusClass }, statusText),
status.running ? E('span', {}, ' (port ' + status.port + ')') : ''
])
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td left' }, _('Version')),
E('div', { 'class': 'td' }, status.version || '-')
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td left' }, _('GitHub Repository')),
E('div', { 'class': 'td' }, status.github_repo || '-')
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td left' }, _('Last Sync')),
E('div', { 'class': 'td' }, status.last_sync || _('Never'))
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td left' }, _('Auto Sync')),
E('div', { 'class': 'td' }, status.auto_sync ?
_('Every %d hours').format(status.sync_interval) : _('Disabled'))
])
])
]);
},
renderArchitectures: function(status) {
var archs = status.architectures || {};
var rows = [];
Object.keys(archs).sort().forEach(function(arch) {
rows.push(E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td left' }, arch),
E('div', { 'class': 'td' }, archs[arch] + ' ' + _('packages'))
]));
});
if (rows.length === 0) {
rows.push(E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'colspan': 2 }, _('No packages synced yet'))
]));
}
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Available Architectures')),
E('div', { 'class': 'table' }, rows)
]);
},
renderActions: function(status) {
var self = this;
var syncBtn = E('button', {
'class': 'cbi-button cbi-button-action',
'click': function() {
ui.showModal(_('Sync Packages'), [
E('p', {}, _('Sync packages from GitHub release?')),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'cbi-button',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': function() {
ui.hideModal();
ui.showModal(_('Syncing...'), [
E('p', { 'class': 'spinning' }, _('Downloading packages from GitHub...'))
]);
callSync(status.version).then(function() {
setTimeout(function() {
ui.hideModal();
window.location.reload();
}, 3000);
});
}
}, _('Sync Now'))
])
]);
}
}, _('Sync Packages'));
var logsBtn = E('button', {
'class': 'cbi-button',
'click': function() {
callLogs(100).then(function(result) {
var logs = (result.logs || '').split('|').join('\n');
ui.showModal(_('Sync Logs'), [
E('pre', { 'style': 'max-height:400px;overflow:auto;font-size:12px;' }, logs || _('No logs')),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'cbi-button',
'click': ui.hideModal
}, _('Close'))
])
]);
});
}
}, _('View Logs'));
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Actions')),
E('div', { 'class': 'cbi-value' }, [
syncBtn, ' ', logsBtn
])
]);
},
renderUsage: function(status) {
var port = status.port || 8888;
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Usage')),
E('p', {}, _('Add to /etc/opkg/customfeeds.conf:')),
E('pre', { 'style': 'background:#f5f5f5;padding:10px;border-radius:4px;' }, [
'# Local repository\n',
'src/gz secubox_luci http://127.0.0.1:' + port + '/luci/{ARCH}\n\n',
'# Or via HTTPS (external)\n',
'src/gz secubox_luci https://repo.secubox.in/luci/{ARCH}'
].join('')),
E('p', {}, _('Replace {ARCH} with your architecture: x86_64, aarch64_cortex-a72, aarch64_generic, etc.'))
]);
},
render: function(data) {
var status = data[0] || {};
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('Package Repository')),
E('div', { 'class': 'cbi-map-descr' },
_('SecuBox package repository - serves OpenWrt packages locally for opkg installation.')),
this.renderStatus(status),
this.renderArchitectures(status),
this.renderActions(status),
this.renderUsage(status)
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,14 @@
{
"admin/services/repo": {
"title": "Package Repository",
"order": 85,
"action": {
"type": "view",
"path": "repo/dashboard"
},
"depends": {
"acl": ["luci-app-repo"],
"uci": {"repo": true}
}
}
}

View File

@ -0,0 +1,17 @@
{
"luci-app-repo": {
"description": "SecuBox Package Repository Dashboard",
"read": {
"ubus": {
"luci.repo": ["status", "config", "packages", "logs"]
},
"uci": ["repo"]
},
"write": {
"ubus": {
"luci.repo": ["sync"]
},
"uci": ["repo"]
}
}
}

View File

@ -0,0 +1,60 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-repo
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=SecuBox Team
PKG_LICENSE:=GPL-3.0
include $(INCLUDE_DIR)/package.mk
define Package/secubox-app-repo
SECTION:=secubox
CATEGORY:=SecuBox
TITLE:=SecuBox Package Repository Manager
DEPENDS:=+uhttpd +wget +gzip +coreutils-stat
PKGARCH:=all
endef
define Package/secubox-app-repo/description
SecuBox Package Repository Manager - hosts and syncs OpenWrt packages
from GitHub releases for local opkg installation.
endef
define Package/secubox-app-repo/conffiles
/etc/config/repo
endef
define Build/Compile
endef
define Package/secubox-app-repo/install
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./root/usr/sbin/repoctl $(1)/usr/sbin/
$(INSTALL_BIN) ./root/usr/sbin/repo-sync $(1)/usr/sbin/
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.repo $(1)/usr/libexec/rpcd/
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-repo.json $(1)/usr/share/rpcd/acl.d/
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./root/etc/init.d/repo-server $(1)/etc/init.d/
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./root/etc/config/repo $(1)/etc/config/
endef
define Package/secubox-app-repo/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
/etc/init.d/repo-server enable
/etc/init.d/repo-server start
/etc/init.d/rpcd restart
}
exit 0
endef
$(eval $(call BuildPackage,secubox-app-repo))

View File

@ -0,0 +1,8 @@
config repo 'main'
option enabled '1'
option version 'v1.0.0-beta'
option github_repo 'gkerma/secubox-openwrt'
option port '8888'
option auto_sync '1'
option sync_interval '6'
option last_sync ''

View File

@ -0,0 +1,29 @@
#!/bin/sh /etc/rc.common
START=99
STOP=10
USE_PROCD=1
REPO_DIR="/srv/repo.secubox.in"
start_service() {
. /lib/functions.sh
config_load repo
config_get enabled main enabled "1"
config_get port main port "8888"
[ "$enabled" = "1" ] || return 0
mkdir -p "$REPO_DIR"
procd_open_instance
procd_set_param command /usr/sbin/uhttpd -f -h "$REPO_DIR" -p "0.0.0.0:$port"
procd_set_param respawn
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
service_triggers() {
procd_add_reload_trigger "repo"
}

View File

@ -0,0 +1,137 @@
#!/bin/sh
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
REPO_DIR="/srv/repo.secubox.in"
LOG_FILE="/var/log/repo-sync.log"
case "$1" in
list)
echo '{"status":{},"config":{},"packages":{"arch":"string"},"sync":{"version":"string"},"logs":{"lines":"number"}}'
;;
call)
case "$2" in
status)
json_init
# Load config
config_load repo
config_get enabled main enabled "1"
config_get version main version "unknown"
config_get github_repo main github_repo ""
config_get port main port "8888"
config_get last_sync main last_sync ""
config_get auto_sync main auto_sync "0"
config_get sync_interval main sync_interval "6"
json_add_boolean "enabled" "$enabled"
json_add_string "version" "$version"
json_add_string "github_repo" "$github_repo"
json_add_int "port" "$port"
json_add_string "last_sync" "$last_sync"
json_add_boolean "auto_sync" "$auto_sync"
json_add_int "sync_interval" "$sync_interval"
# Server status
if netstat -tln 2>/dev/null | grep -q ":$port "; then
json_add_boolean "running" 1
else
json_add_boolean "running" 0
fi
# Package counts by architecture
json_add_object "architectures"
for dir in "$REPO_DIR/luci"/*; do
[ -d "$dir" ] || continue
arch=$(basename "$dir")
count=$(ls "$dir"/*.ipk 2>/dev/null | wc -l)
json_add_int "$arch" "$count"
done
json_close_object
json_dump
;;
config)
json_init
config_load repo
config_get enabled main enabled "1"
config_get version main version ""
config_get github_repo main github_repo ""
config_get port main port "8888"
config_get auto_sync main auto_sync "0"
config_get sync_interval main sync_interval "6"
json_add_boolean "enabled" "$enabled"
json_add_string "version" "$version"
json_add_string "github_repo" "$github_repo"
json_add_int "port" "$port"
json_add_boolean "auto_sync" "$auto_sync"
json_add_int "sync_interval" "$sync_interval"
json_dump
;;
packages)
read -r input
json_load "$input"
json_get_var arch arch "x86_64"
json_init
json_add_string "arch" "$arch"
json_add_array "packages"
dir="$REPO_DIR/luci/$arch"
if [ -d "$dir" ]; then
for ipk in "$dir"/*.ipk; do
[ -f "$ipk" ] || continue
name=$(basename "$ipk")
size=$(stat -c%s "$ipk" 2>/dev/null || echo 0)
json_add_object ""
json_add_string "name" "$name"
json_add_int "size" "$size"
json_close_object
done
fi
json_close_array
json_dump
;;
sync)
read -r input
json_load "$input"
json_get_var version version ""
if [ -n "$version" ]; then
uci set repo.main.version="$version"
uci commit repo
fi
# Run sync in background
/usr/sbin/repo-sync &
json_init
json_add_boolean "started" 1
json_add_string "message" "Sync started in background"
json_dump
;;
logs)
read -r input
json_load "$input"
json_get_var lines lines 50
json_init
if [ -f "$LOG_FILE" ]; then
json_add_string "logs" "$(tail -n "$lines" "$LOG_FILE" | sed 's/"/\\"/g' | tr '\n' '|')"
else
json_add_string "logs" ""
fi
json_dump
;;
esac
;;
esac

View File

@ -0,0 +1,131 @@
#!/bin/sh
# SecuBox Package Repository Sync Script
# Syncs packages from GitHub releases to local repo
. /lib/functions.sh
REPO_DIR="/srv/repo.secubox.in"
CONFIG_FILE="/etc/config/repo"
LOG_FILE="/var/log/repo-sync.log"
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg"
echo "$msg" >> "$LOG_FILE"
}
# Load config
config_load repo
config_get GITHUB_REPO main github_repo "gkerma/secubox-openwrt"
config_get VERSION main version "v1.0.0-beta"
config_get ENABLED main enabled "1"
[ "$ENABLED" = "1" ] || { log "Repo sync disabled"; exit 0; }
VERSION_NUM="${VERSION#v}"
TMP_DIR="/tmp/repo-sync-$$"
log "Starting sync from $GITHUB_REPO $VERSION"
mkdir -p "$TMP_DIR"
mkdir -p "$REPO_DIR/packages" "$REPO_DIR/luci" "$REPO_DIR/catalog"
cd "$TMP_DIR"
# Architecture mappings: github-arch:opkg-arch
ARCHS="x86-64:x86_64 aarch64-generic:aarch64_generic aarch64-cortex-a72:aarch64_cortex-a72 rockchip-armv8:aarch64_generic mips-24kc:mips_24kc mipsel-24kc:mipsel_24kc"
for arch_map in $ARCHS; do
ARCH="${arch_map%%:*}"
OPKG_ARCH="${arch_map##*:}"
TARBALL="secubox-${VERSION_NUM}-${ARCH}.tar.gz"
URL="https://github.com/${GITHUB_REPO}/releases/download/${VERSION}/${TARBALL}"
log "Downloading $TARBALL..."
if wget -q -O "$TARBALL" "$URL" 2>/dev/null; then
mkdir -p "$REPO_DIR/packages/$OPKG_ARCH"
mkdir -p "$REPO_DIR/luci/$OPKG_ARCH"
# Extract
mkdir -p "extract-$ARCH"
tar -xzf "$TARBALL" -C "extract-$ARCH" 2>/dev/null
# Sort packages
find "extract-$ARCH" -name '*.ipk' | while read pkg; do
PKG_NAME="$(basename "$pkg")"
if echo "$PKG_NAME" | grep -q '^luci-'; then
cp "$pkg" "$REPO_DIR/luci/$OPKG_ARCH/"
else
cp "$pkg" "$REPO_DIR/packages/$OPKG_ARCH/"
fi
done
log " Extracted to $OPKG_ARCH"
else
log " Skipping $ARCH (not found)"
fi
done
# Generate Packages index
log "Generating opkg indexes..."
for basedir in "$REPO_DIR/packages" "$REPO_DIR/luci"; do
for dir in "$basedir"/*; do
[ -d "$dir" ] || continue
cd "$dir"
rm -f Packages Packages.gz
for ipk in *.ipk 2>/dev/null; do
[ -f "$ipk" ] || continue
SIZE=$(stat -c%s "$ipk" 2>/dev/null || ls -l "$ipk" | awk '{print $5}')
MD5=$(md5sum "$ipk" | cut -d' ' -f1)
PKG=$(echo "$ipk" | sed 's/_.*//g')
echo "Package: $PKG"
echo "Version: 0.0.0-r1"
echo "Architecture: all"
echo "Filename: $ipk"
echo "Size: $SIZE"
echo "MD5Sum: $MD5"
echo ""
done > Packages
gzip -9c Packages > Packages.gz
log " $(basename "$dir"): $(grep -c '^Package:' Packages 2>/dev/null || echo 0) packages"
done
done
# Create index.html
cat > "$REPO_DIR/index.html" << 'HTML'
<!DOCTYPE html>
<html><head><title>SecuBox Package Repository</title>
<style>
body { font-family: sans-serif; max-width: 800px; margin: 2em auto; padding: 0 1em; }
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; }
pre { background: #f0f0f0; padding: 1em; overflow-x: auto; }
</style>
</head>
<body>
<h1>SecuBox Package Repository</h1>
<p>Add to <code>/etc/opkg/customfeeds.conf</code>:</p>
<pre>src/gz secubox_packages https://repo.secubox.in/packages/{ARCH}
src/gz secubox_luci https://repo.secubox.in/luci/{ARCH}</pre>
<h2>Architectures</h2>
<ul>
<li><a href="luci/x86_64/">x86_64</a> - x86-64 VMs</li>
<li><a href="luci/aarch64_cortex-a72/">aarch64_cortex-a72</a> - Raspberry Pi 4</li>
<li><a href="luci/aarch64_generic/">aarch64_generic</a> - NanoPi R4S/R5S</li>
<li><a href="luci/mips_24kc/">mips_24kc</a> - Atheros/QCA</li>
<li><a href="luci/mipsel_24kc/">mipsel_24kc</a> - MT7621</li>
</ul>
</body></html>
HTML
# Cleanup
cd /
rm -rf "$TMP_DIR"
# Update last sync time
uci set repo.main.last_sync="$(date -Iseconds)"
uci commit repo
log "Sync complete"

View File

@ -0,0 +1,156 @@
#!/bin/sh
# SecuBox Package Repository Control Tool
. /lib/functions.sh
REPO_DIR="/srv/repo.secubox.in"
CONFIG="repo"
usage() {
cat << EOF
Usage: repoctl <command> [options]
Commands:
status Show repository status
sync [version] Sync packages from GitHub release
start Start repository server
stop Stop repository server
restart Restart repository server
list [arch] List packages for architecture
config Show current configuration
set <key> <val> Set configuration value
Configuration keys:
enabled Enable/disable repo (0/1)
version Release version to sync (e.g., v1.0.0-beta)
github_repo GitHub repository (e.g., gkerma/secubox-openwrt)
port Server port (default: 8888)
auto_sync Enable auto-sync cron (0/1)
sync_interval Sync interval in hours (default: 6)
Examples:
repoctl status
repoctl sync v1.0.0-beta
repoctl list x86_64
repoctl set version v1.0.1
EOF
exit 1
}
cmd_status() {
config_load "$CONFIG"
config_get enabled main enabled "1"
config_get version main version "unknown"
config_get github_repo main github_repo "gkerma/secubox-openwrt"
config_get port main port "8888"
config_get last_sync main last_sync "never"
config_get auto_sync main auto_sync "0"
echo "SecuBox Package Repository Status"
echo "=================================="
echo "Enabled: $enabled"
echo "Version: $version"
echo "GitHub Repo: $github_repo"
echo "Server Port: $port"
echo "Last Sync: $last_sync"
echo "Auto Sync: $auto_sync"
echo ""
# Check server status
if netstat -tln 2>/dev/null | grep -q ":$port "; then
echo "Server: RUNNING (port $port)"
else
echo "Server: STOPPED"
fi
# Package counts
echo ""
echo "Packages:"
for dir in "$REPO_DIR/luci"/*; do
[ -d "$dir" ] || continue
arch=$(basename "$dir")
count=$(ls "$dir"/*.ipk 2>/dev/null | wc -l)
echo " $arch: $count packages"
done
}
cmd_sync() {
local version="${1:-}"
if [ -n "$version" ]; then
uci set "$CONFIG.main.version=$version"
uci commit "$CONFIG"
fi
/usr/sbin/repo-sync
}
cmd_start() {
/etc/init.d/repo-server start
echo "Repository server started"
}
cmd_stop() {
/etc/init.d/repo-server stop
echo "Repository server stopped"
}
cmd_restart() {
/etc/init.d/repo-server restart
echo "Repository server restarted"
}
cmd_list() {
local arch="${1:-x86_64}"
local dir="$REPO_DIR/luci/$arch"
if [ ! -d "$dir" ]; then
echo "Architecture '$arch' not found"
echo "Available: $(ls "$REPO_DIR/luci" 2>/dev/null | tr '\n' ' ')"
exit 1
fi
echo "Packages for $arch:"
ls "$dir"/*.ipk 2>/dev/null | while read pkg; do
basename "$pkg"
done
}
cmd_config() {
uci show "$CONFIG"
}
cmd_set() {
local key="$1"
local value="$2"
[ -z "$key" ] || [ -z "$value" ] && usage
uci set "$CONFIG.main.$key=$value"
uci commit "$CONFIG"
echo "Set $key = $value"
# Handle auto_sync changes
if [ "$key" = "auto_sync" ]; then
if [ "$value" = "1" ]; then
config_get interval main sync_interval "6"
echo "0 */$interval * * * /usr/sbin/repo-sync >/dev/null 2>&1" > /etc/cron.d/repo-sync
echo "Enabled auto-sync every $interval hours"
else
rm -f /etc/cron.d/repo-sync
echo "Disabled auto-sync"
fi
/etc/init.d/cron restart 2>/dev/null
fi
}
# Main
case "$1" in
status) cmd_status ;;
sync) cmd_sync "$2" ;;
start) cmd_start ;;
stop) cmd_stop ;;
restart) cmd_restart ;;
list) cmd_list "$2" ;;
config) cmd_config ;;
set) cmd_set "$2" "$3" ;;
*) usage ;;
esac

View File

@ -0,0 +1,17 @@
{
"luci-app-repo": {
"description": "SecuBox Package Repository",
"read": {
"ubus": {
"luci.repo": ["status", "config", "packages", "logs"]
},
"uci": ["repo"]
},
"write": {
"ubus": {
"luci.repo": ["sync"]
},
"uci": ["repo"]
}
}
}