diff --git a/package/secubox/luci-app-nextcloud/htdocs/luci-static/resources/view/nextcloud/overview.js b/package/secubox/luci-app-nextcloud/htdocs/luci-static/resources/view/nextcloud/overview.js index 4700d7d6..b77aa8fe 100644 --- a/package/secubox/luci-app-nextcloud/htdocs/luci-static/resources/view/nextcloud/overview.js +++ b/package/secubox/luci-app-nextcloud/htdocs/luci-static/resources/view/nextcloud/overview.js @@ -91,6 +91,25 @@ var callLogs = rpc.declare({ expect: {} }); +var callGetStorage = rpc.declare({ + object: 'luci.nextcloud', + method: 'get_storage', + expect: {} +}); + +var callDeleteBackup = rpc.declare({ + object: 'luci.nextcloud', + method: 'delete_backup', + params: ['name'], + expect: {} +}); + +var callUninstall = rpc.declare({ + object: 'luci.nextcloud', + method: 'uninstall', + expect: {} +}); + var callListUsers = rpc.declare({ object: 'luci.nextcloud', method: 'list_users', @@ -134,6 +153,7 @@ return view.extend({ config: {}, backups: [], users: [], + storage: {}, currentTab: 'overview', load: function() { @@ -141,7 +161,8 @@ return view.extend({ callStatus(), callGetConfig(), callListBackups().catch(function() { return { backups: [] }; }), - callListUsers().catch(function() { return { users: [] }; }) + callListUsers().catch(function() { return { users: [] }; }), + callGetStorage().catch(function() { return {}; }) ]); }, @@ -151,6 +172,7 @@ return view.extend({ this.config = data[1] || {}; this.backups = (data[2] || {}).backups || []; this.users = (data[3] || {}).users || []; + this.storage = data[4] || {}; // Not installed - show install view if (!this.status.installed) { @@ -161,6 +183,7 @@ return view.extend({ var tabs = [ { id: 'overview', label: 'Overview', icon: 'πŸŽ›οΈ' }, { id: 'users', label: 'Users', icon: 'πŸ‘₯' }, + { id: 'storage', label: 'Storage', icon: 'πŸ’Ώ' }, { id: 'backups', label: 'Backups', icon: 'πŸ’Ύ' }, { id: 'ssl', label: 'SSL', icon: 'πŸ”’' }, { id: 'logs', label: 'Logs', icon: 'πŸ“œ' } @@ -215,6 +238,7 @@ return view.extend({ renderTabContent: function() { switch (this.currentTab) { case 'users': return this.renderUsersTab(); + case 'storage': return this.renderStorageTab(); case 'backups': return this.renderBackupsTab(); case 'ssl': return this.renderSSLTab(); case 'logs': return this.renderLogsTab(); @@ -480,6 +504,63 @@ return view.extend({ }); }, + // ======================================================================== + // Storage Tab + // ======================================================================== + + renderStorageTab: function() { + var self = this; + var st = this.storage; + + return E('div', {}, [ + // Storage Stats + E('div', { 'class': 'kiss-grid kiss-grid-4', 'style': 'margin-bottom:24px;' }, [ + KissTheme.stat(st.data_size || '0', 'User Data', 'var(--kiss-cyan)'), + KissTheme.stat(st.backup_size || '0', 'Backups', 'var(--kiss-purple)'), + KissTheme.stat(st.disk_free || '0', 'Disk Free', 'var(--kiss-green)'), + KissTheme.stat((st.disk_used_percent || 0) + '%', 'Disk Used', st.disk_used_percent > 80 ? 'var(--kiss-red)' : 'var(--kiss-blue)') + ]), + + // Disk Usage Bar + KissTheme.card([ + E('span', {}, 'πŸ’Ώ Disk Usage') + ], E('div', {}, [ + E('div', { 'style': 'display:flex;justify-content:space-between;margin-bottom:8px;font-size:13px;' }, [ + E('span', {}, 'Used: ' + (st.disk_used_percent || 0) + '%'), + E('span', { 'style': 'color:var(--kiss-muted);' }, st.disk_free + ' free of ' + st.disk_total) + ]), + E('div', { 'style': 'height:24px;background:var(--kiss-bg2);border-radius:12px;overflow:hidden;' }, [ + E('div', { + 'style': 'height:100%;width:' + (st.disk_used_percent || 0) + '%;background:linear-gradient(90deg, var(--kiss-cyan), var(--kiss-blue));transition:width 0.3s;' + }) + ]) + ])), + + // Storage Details + KissTheme.card([ + E('span', {}, 'πŸ“Š Storage Breakdown') + ], E('div', { 'style': 'display:flex;flex-direction:column;gap:12px;' }, [ + this.storageRow('πŸ“ User Data', st.data_size || '0', 'var(--kiss-cyan)'), + this.storageRow('πŸ’Ύ Backups', st.backup_size || '0', 'var(--kiss-purple)'), + this.storageRow('πŸ“¦ Total Nextcloud', st.total_size || '0', 'var(--kiss-blue)') + ])), + + // Data Path Info + KissTheme.card([ + E('span', {}, 'ℹ️ Data Location') + ], E('div', { 'style': 'font-family:monospace;padding:12px;background:var(--kiss-bg2);border-radius:6px;' }, + this.status.data_path || '/srv/nextcloud' + )) + ]); + }, + + storageRow: function(label, size, color) { + return E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;padding:10px;background:var(--kiss-bg2);border-radius:6px;' }, [ + E('span', { 'style': 'display:flex;align-items:center;gap:8px;' }, label), + E('span', { 'style': 'font-weight:600;color:' + color + ';' }, size) + ]); + }, + // ======================================================================== // Backups Tab // ======================================================================== @@ -531,19 +612,27 @@ return view.extend({ E('th', {}, 'Name'), E('th', {}, 'Size'), E('th', {}, 'Date'), - E('th', {}, 'Actions') + E('th', { 'style': 'width:150px;' }, 'Actions') ])), E('tbody', {}, this.backups.map(function(b) { return E('tr', {}, [ E('td', { 'style': 'font-family:monospace;' }, b.name), E('td', {}, b.size || '-'), E('td', {}, fmtRelative(b.timestamp)), - E('td', {}, E('button', { - 'class': 'kiss-btn kiss-btn-blue', - 'style': 'padding:4px 10px;font-size:11px;', - 'data-name': b.name, - 'click': function(ev) { self.handleRestore(ev.currentTarget.dataset.name); } - }, '⬇️ Restore')) + E('td', { 'style': 'display:flex;gap:6px;' }, [ + E('button', { + 'class': 'kiss-btn kiss-btn-blue', + 'style': 'padding:4px 10px;font-size:11px;', + 'data-name': b.name, + 'click': function(ev) { self.handleRestore(ev.currentTarget.dataset.name); } + }, '⬇️ Restore'), + E('button', { + 'class': 'kiss-btn kiss-btn-red', + 'style': 'padding:4px 10px;font-size:11px;', + 'data-name': b.name, + 'click': function(ev) { self.handleDeleteBackup(ev.currentTarget.dataset.name); } + }, 'πŸ—‘οΈ') + ]) ]); })) ]); @@ -810,6 +899,30 @@ return view.extend({ }); }, + handleDeleteBackup: function(name) { + var self = this; + if (!confirm('Delete backup "' + name + '"? This cannot be undone.')) { + return; + } + + ui.showModal('Deleting Backup', [ + E('p', { 'class': 'spinning' }, 'Deleting ' + name + '...') + ]); + + callDeleteBackup(name).then(function(r) { + ui.hideModal(); + if (r.success) { + ui.addNotification(null, E('p', 'Backup deleted: ' + name), 'info'); + self.refreshBackups(); + } else { + ui.addNotification(null, E('p', 'Failed: ' + (r.error || 'Unknown')), 'error'); + } + }).catch(function(e) { + ui.hideModal(); + ui.addNotification(null, E('p', 'Error: ' + e.message), 'error'); + }); + }, + refreshBackups: function() { var self = this; callListBackups().then(function(data) { @@ -839,11 +952,13 @@ return view.extend({ return Promise.all([ callStatus(), callListBackups().catch(function() { return { backups: [] }; }), - callListUsers().catch(function() { return { users: [] }; }) + callListUsers().catch(function() { return { users: [] }; }), + callGetStorage().catch(function() { return {}; }) ]).then(function(data) { self.status = data[0] || {}; self.backups = (data[1] || {}).backups || []; self.users = (data[2] || {}).users || []; + self.storage = data[3] || {}; // Update tab content var tabContent = document.getElementById('tab-content'); diff --git a/package/secubox/luci-app-nextcloud/root/usr/libexec/rpcd/luci.nextcloud b/package/secubox/luci-app-nextcloud/root/usr/libexec/rpcd/luci.nextcloud index 2f7d3a61..d4f6d92e 100755 --- a/package/secubox/luci-app-nextcloud/root/usr/libexec/rpcd/luci.nextcloud +++ b/package/secubox/luci-app-nextcloud/root/usr/libexec/rpcd/luci.nextcloud @@ -370,6 +370,74 @@ reset_password() { fi } +# Uninstall Nextcloud +do_uninstall() { + if command -v nextcloudctl >/dev/null 2>&1; then + nextcloudctl uninstall >/tmp/nextcloud-uninstall.log 2>&1 + echo '{"success": true, "message": "Nextcloud uninstalled (data preserved)"}' + else + echo '{"success": false, "error": "nextcloudctl not found"}' + fi +} + +# Get storage stats +get_storage() { + local data_path=$(uci_get main.data_path) + data_path="${data_path:-/srv/nextcloud}" + + local total_size="0" + local data_size="0" + local backup_size="0" + + if [ -d "$data_path" ]; then + total_size=$(du -sh "$data_path" 2>/dev/null | awk '{print $1}' || echo "0") + fi + if [ -d "$data_path/data" ]; then + data_size=$(du -sh "$data_path/data" 2>/dev/null | awk '{print $1}' || echo "0") + fi + if [ -d "$data_path/backups" ]; then + backup_size=$(du -sh "$data_path/backups" 2>/dev/null | awk '{print $1}' || echo "0") + fi + + # Get disk free space + local disk_free=$(df -h "$data_path" 2>/dev/null | tail -1 | awk '{print $4}' || echo "0") + local disk_total=$(df -h "$data_path" 2>/dev/null | tail -1 | awk '{print $2}' || echo "0") + local disk_used_pct=$(df "$data_path" 2>/dev/null | tail -1 | awk '{print $5}' | tr -d '%' || echo "0") + + cat </dev/null) + + if [ -z "$name" ]; then + echo '{"success": false, "error": "No backup name specified"}' + return + fi + + local data_path=$(uci_get main.data_path) + local backup_dir="${data_path:-/srv/nextcloud}/backups" + + if [ -f "$backup_dir/$name-db.sql" ]; then + rm -f "$backup_dir/$name-db.sql" "$backup_dir/$name-data.tar.gz" + echo '{"success": true, "message": "Backup deleted: '"$name"'"}' + else + echo '{"success": false, "error": "Backup not found"}' + fi +} + # RPCD list method list_methods() { cat <<'EOF' @@ -378,6 +446,7 @@ list_methods() { "get_config": {}, "save_config": {"http_port": "string", "data_path": "string", "domain": "string", "admin_user": "string", "memory_limit": "string", "upload_max": "string"}, "install": {}, + "uninstall": {}, "start": {}, "stop": {}, "restart": {}, @@ -385,12 +454,14 @@ list_methods() { "backup": {"name": "string"}, "restore": {"name": "string"}, "list_backups": {}, + "delete_backup": {"name": "string"}, "ssl_enable": {"domain": "string"}, "ssl_disable": {}, "occ": {"command": "string"}, "logs": {}, "list_users": {}, - "reset_password": {"uid": "string", "password": "string"} + "reset_password": {"uid": "string", "password": "string"}, + "get_storage": {} } EOF } @@ -406,6 +477,7 @@ case "$1" in get_config) get_config ;; save_config) save_config ;; install) do_install ;; + uninstall) do_uninstall ;; start) do_start ;; stop) do_stop ;; restart) do_restart ;; @@ -413,12 +485,14 @@ case "$1" in backup) do_backup ;; restore) do_restore ;; list_backups) list_backups ;; + delete_backup) delete_backup ;; ssl_enable) do_ssl_enable ;; ssl_disable) do_ssl_disable ;; occ) do_occ ;; logs) get_logs ;; list_users) list_users ;; reset_password) reset_password ;; + get_storage) get_storage ;; *) echo '{"error": "Unknown method"}' ;; esac ;; diff --git a/package/secubox/luci-app-nextcloud/root/usr/share/rpcd/acl.d/luci-app-nextcloud.json b/package/secubox/luci-app-nextcloud/root/usr/share/rpcd/acl.d/luci-app-nextcloud.json index 36ff9cc6..92f762a5 100644 --- a/package/secubox/luci-app-nextcloud/root/usr/share/rpcd/acl.d/luci-app-nextcloud.json +++ b/package/secubox/luci-app-nextcloud/root/usr/share/rpcd/acl.d/luci-app-nextcloud.json @@ -3,7 +3,7 @@ "description": "Grant access to Nextcloud LXC app", "read": { "ubus": { - "luci.nextcloud": ["status", "get_config", "list_backups", "logs", "list_users"] + "luci.nextcloud": ["status", "get_config", "list_backups", "logs", "list_users", "get_storage"] }, "uci": ["nextcloud"] }, @@ -11,6 +11,7 @@ "ubus": { "luci.nextcloud": [ "install", + "uninstall", "start", "stop", "restart", @@ -18,6 +19,7 @@ "save_config", "backup", "restore", + "delete_backup", "ssl_enable", "ssl_disable", "occ", diff --git a/package/secubox/secubox-app-nextcloud/README.md b/package/secubox/secubox-app-nextcloud/README.md index 5966489a..e58dab11 100644 --- a/package/secubox/secubox-app-nextcloud/README.md +++ b/package/secubox/secubox-app-nextcloud/README.md @@ -1,11 +1,24 @@ # SecuBox Nextcloud -Self-hosted file sync and share platform running in Docker on OpenWrt. Provides calendar, contacts, collaborative editing, and file management. +Self-hosted file sync and collaboration platform running in a Debian LXC container on OpenWrt. Features MariaDB database, Redis caching, and Nginx web server. ## Installation ```bash -opkg install secubox-app-nextcloud +opkg install secubox-app-nextcloud luci-app-nextcloud +``` + +## Quick Start + +```bash +# Install Nextcloud (creates LXC container) +nextcloudctl install + +# Start service +/etc/init.d/nextcloud start + +# Access web interface +# http://router-ip:8080 ``` ## Configuration @@ -15,21 +28,55 @@ UCI config file: `/etc/config/nextcloud` ```bash uci set nextcloud.main.enabled='1' uci set nextcloud.main.domain='cloud.example.com' -uci set nextcloud.main.port='8080' +uci set nextcloud.main.http_port='8080' uci set nextcloud.main.admin_user='admin' -uci set nextcloud.main.data_dir='/srv/nextcloud/data' +uci set nextcloud.main.memory_limit='1G' +uci set nextcloud.main.upload_max='512M' uci commit nextcloud ``` -## Usage +## CLI Commands ```bash -nextcloudctl start # Start Nextcloud container -nextcloudctl stop # Stop Nextcloud container -nextcloudctl status # Show service status -nextcloudctl update # Pull latest container image -nextcloudctl occ # Run Nextcloud occ command -nextcloudctl logs # View container logs +nextcloudctl install # Create Debian LXC, install Nextcloud stack +nextcloudctl uninstall # Remove container (preserves data) +nextcloudctl update # Update Nextcloud to latest version +nextcloudctl start # Start Nextcloud service +nextcloudctl stop # Stop Nextcloud service +nextcloudctl restart # Restart Nextcloud service +nextcloudctl status # Show service status (JSON) +nextcloudctl logs [-f] # Show container logs +nextcloudctl shell # Open shell in container + +nextcloudctl occ # Run Nextcloud OCC command +nextcloudctl backup [name] # Create backup of data and database +nextcloudctl restore # Restore from backup +nextcloudctl list-backups # List available backups + +nextcloudctl ssl-enable # Register with HAProxy for SSL +nextcloudctl ssl-disable # Remove HAProxy registration +``` + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ OpenWrt Host β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ LXC: nextcloud (Debian 12) β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ Nginx β”‚ β”‚Nextcloudβ”‚ β”‚ MariaDB β”‚ β”‚ Redis β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ :8080 β”‚β†’ β”‚ PHP-FPM β”‚β†’ β”‚ :3306 β”‚ β”‚ :6379 β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ ↓ β”‚ β”‚ +β”‚ β”‚ /srv/nextcloud (bind mount) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ ↓ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ HAProxy (optional SSL termination) β”‚ β”‚ +β”‚ β”‚ cloud.example.com:443 β†’ nextcloud:8080 β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ## Features @@ -37,13 +84,42 @@ nextcloudctl logs # View container logs - File sync and share with web, desktop, and mobile clients - Calendar and contacts (CalDAV/CardDAV) - Collaborative document editing -- Docker-based deployment with persistent storage +- End-to-end encryption support +- Debian LXC container with PHP 8.2 +- MariaDB database with optimized settings +- Redis caching for improved performance +- Nginx with Nextcloud-optimized config +- HAProxy integration for SSL/HTTPS +- Automated backup and restore +- Memory limit via cgroups +- Auto-start on boot + +## Data Locations + +``` +/srv/nextcloud/ +β”œβ”€β”€ data/ # Nextcloud user data +β”œβ”€β”€ config/ # Nextcloud config.php +└── backups/ # Automated backups +``` + +## SSL with HAProxy + +```bash +# Enable HTTPS via HAProxy with Let's Encrypt +nextcloudctl ssl-enable cloud.example.com + +# Access via HTTPS +https://cloud.example.com +``` ## Dependencies -- `dockerd` -- `docker` -- `containerd` +- `lxc` - Container runtime +- `lxc-common` - LXC utilities +- `tar`, `wget-ssl`, `unzip`, `xz` - Archive tools +- `jsonfilter` - JSON parsing +- `openssl-util` - SSL utilities ## License diff --git a/package/secubox/secubox-app-nextcloud/files/usr/sbin/nextcloudctl b/package/secubox/secubox-app-nextcloud/files/usr/sbin/nextcloudctl index fb08c309..357a2707 100644 --- a/package/secubox/secubox-app-nextcloud/files/usr/sbin/nextcloudctl +++ b/package/secubox/secubox-app-nextcloud/files/usr/sbin/nextcloudctl @@ -6,7 +6,7 @@ CONFIG="nextcloud" LXC_NAME="nextcloud" -NEXTCLOUD_VERSION="30.0.4" +NEXTCLOUD_VERSION="31.0.5" # Paths LXC_PATH="/srv/lxc" @@ -397,9 +397,16 @@ server { return 301 /index.php$request_uri; } - location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) { return 404; } + location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)(?!.*\.(css|js|svg|png|gif|ico|woff2?)$) { return 404; } location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; } + # Serve app static files correctly + location ~ ^/apps/[^/]+/(?:css|js|img|l10n)/.+$ { + try_files \$uri /index.php\$request_uri; + add_header Cache-Control "public, max-age=15778463, immutable"; + access_log off; + } + location ~ \.php(?:$|/) { rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php$request_uri; @@ -543,6 +550,10 @@ lxc.uts.name = $LXC_NAME lxc.rootfs.path = dir:$LXC_ROOTFS lxc.arch = $(uname -m) +# Auto-start on boot +lxc.start.auto = 1 +lxc.start.delay = 5 + # Network: use host network lxc.net.0.type = none @@ -557,6 +568,9 @@ lxc.environment = NEXTCLOUD_HTTP_PORT=$http_port lxc.environment = NEXTCLOUD_UPLOAD_MAX=$upload_max lxc.environment = NEXTCLOUD_TRUSTED_PROXIES=$trusted_proxies +# Memory limit +lxc.cgroup2.memory.max = $mem_bytes + # Security lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time sys_rawio