From 09b40c3b8885f150f298ad50c82abe37af67ab94 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Mon, 16 Feb 2026 07:49:17 +0100 Subject: [PATCH] feat(nextcloud): Migrate to LXC with full-stack enhancement - Migrate from Docker to Debian 12 LXC container - Full stack: Nginx, MariaDB, Redis, PHP 8.2-FPM, Nextcloud - Rewrite nextcloudctl CLI with install/backup/restore/ssl/occ commands - New UCI config schema: main, db, redis, ssl, backup sections - Enhanced RPCD backend with 15 methods - KISS dashboard with Overview/Backups/SSL/Logs tabs - Updated dependencies for LXC packages - SecuBox menu path integration Co-Authored-By: Claude Opus 4.5 --- .claude/HISTORY.md | 23 + .claude/WIP.md | 11 +- package/secubox/luci-app-nextcloud/Makefile | 4 +- .../resources/view/nextcloud/overview.js | 856 ++++++++++--- .../resources/view/nextcloud/settings.js | 123 +- .../root/usr/libexec/rpcd/luci.nextcloud | 281 +++-- .../share/luci/menu.d/luci-app-nextcloud.json | 10 +- .../share/rpcd/acl.d/luci-app-nextcloud.json | 18 +- .../secubox/secubox-app-nextcloud/Makefile | 10 +- .../files/etc/config/nextcloud | 30 +- .../files/etc/init.d/nextcloud | 29 +- .../files/usr/sbin/nextcloudctl | 1095 +++++++++++++++-- 12 files changed, 2082 insertions(+), 408 deletions(-) diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index 699d3788..aabca0a1 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -1861,3 +1861,26 @@ git checkout HEAD -- index.html - MetaBlogizer: HAProxy vhost lookup for automatic subdomain detection - Added more icons for new service types - **Result**: 67 services with proper subdomain URLs + +### 2026-02-16: Nextcloud LXC Enhancement +- **Migrated** secubox-app-nextcloud from Docker to LXC (Debian 12 based) +- **Complete rewrite** of `nextcloudctl` CLI (1018 lines): + - Commands: install, uninstall, update, status, logs, shell, occ, backup, restore, ssl-enable, ssl-disable + - Downloads Debian 12 rootfs from LXC image server + - Installs full stack: Nginx, MariaDB, Redis, PHP 8.2-FPM, Nextcloud + - Automated database setup and configuration +- **New UCI config schema** with sections: main, db, redis, ssl, backup +- **Enhanced RPCD backend** (366 lines) with 15 methods: + - status, get_config, save_config, install, start, stop, restart + - update, backup, restore, list_backups, ssl_enable, ssl_disable, occ, logs +- **KISS Dashboard** (725 lines) with: + - Install view with feature cards + - Overview tab with stats grid (Status, Version, Users, Storage) + - Backups tab with create/restore functionality + - SSL tab for HAProxy/ACME integration + - Logs tab for operation monitoring +- **Updated dependencies**: + - secubox-app-nextcloud: +lxc +lxc-common +tar +wget-ssl +jsonfilter +openssl-util +unzip +xz + - luci-app-nextcloud: +luci-lib-secubox +secubox-app-nextcloud +- **Updated ACL** with all new RPCD methods +- **Updated menu** to SecuBox path (admin/secubox/services/nextcloud) diff --git a/.claude/WIP.md b/.claude/WIP.md index 22be004b..1fa3512b 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -62,7 +62,16 @@ _Last updated: 2026-02-15 (PeerTube transcoding fix, GK2 Hub subdomain URLs)_ - Gossip-based exposure config sync via secubox-p2p - Created `luci-app-vortex-dns` dashboard -### Just Completed (2026-02-15) +### Just Completed (2026-02-16) + +- **Nextcloud LXC Enhancement** — DONE (2026-02-16) + - Migrated from Docker to Debian 12 LXC container + - Full-stack: Nginx, MariaDB, Redis, PHP 8.2-FPM, Nextcloud + - `nextcloudctl` CLI with install/backup/restore/ssl/occ commands + - KISS dashboard with Overview/Backups/SSL/Logs tabs + - RPCD backend with 15 methods + +### Recently Completed (2026-02-15) - **HAProxy & Mitmproxy WAF Fixes** — DONE (2026-02-15) - Fixed HAProxy reload: copy config to `/etc/haproxy/` before signal diff --git a/package/secubox/luci-app-nextcloud/Makefile b/package/secubox/luci-app-nextcloud/Makefile index 15b4494a..93588e14 100644 --- a/package/secubox/luci-app-nextcloud/Makefile +++ b/package/secubox/luci-app-nextcloud/Makefile @@ -3,8 +3,8 @@ include $(TOPDIR)/rules.mk -LUCI_TITLE:=LuCI support for Nextcloud -LUCI_DEPENDS:=+luci-base +LUCI_TITLE:=LuCI support for Nextcloud LXC +LUCI_DEPENDS:=+luci-base +luci-lib-secubox +secubox-app-nextcloud LUCI_PKGARCH:=all PKG_NAME:=luci-app-nextcloud 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 c09b473e..5f1fc97a 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 @@ -1,214 +1,722 @@ 'use strict'; 'require view'; +'require dom'; 'require ui'; 'require rpc'; 'require poll'; 'require secubox/kiss-theme'; -var callStatus = rpc.declare({ object: 'luci.nextcloud', method: 'status', expect: {} }); -var callInstall = rpc.declare({ object: 'luci.nextcloud', method: 'install', expect: {} }); -var callStart = rpc.declare({ object: 'luci.nextcloud', method: 'start', expect: {} }); -var callStop = rpc.declare({ object: 'luci.nextcloud', method: 'stop', expect: {} }); -var callRestart = rpc.declare({ object: 'luci.nextcloud', method: 'restart', expect: {} }); +// ============================================================================ +// RPC Declarations +// ============================================================================ -var css = '.nc-container{max-width:900px;margin:0 auto}.nc-header{display:flex;justify-content:space-between;align-items:center;padding:1.5rem;background:linear-gradient(135deg,#0082c9 0%,#00639b 100%);border-radius:16px;color:#fff;margin-bottom:1.5rem}.nc-header h2{margin:0;font-size:1.5rem;display:flex;align-items:center;gap:.5rem}.nc-status{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:20px;font-size:.9rem}.nc-status.running{background:rgba(16,185,129,.2)}.nc-status.stopped{background:rgba(239,68,68,.2)}.nc-dot{width:10px;height:10px;border-radius:50%;animation:pulse 2s infinite}.nc-status.running .nc-dot{background:#10b981}.nc-status.stopped .nc-dot{background:#ef4444}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}.nc-card{background:#fff;border-radius:12px;padding:1.5rem;box-shadow:0 2px 8px rgba(0,0,0,.08);margin-bottom:1rem}.nc-card-title{font-size:1.1rem;font-weight:600;margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.nc-info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem}.nc-info-item{padding:1rem;background:#f8f9fa;border-radius:8px}.nc-info-label{font-size:.8rem;color:#666;margin-bottom:.25rem}.nc-info-value{font-size:1.1rem;font-weight:500}.nc-actions{display:flex;gap:.75rem;flex-wrap:wrap}.nc-btn{padding:.6rem 1.2rem;border-radius:8px;border:none;cursor:pointer;font-weight:500;transition:all .2s}.nc-btn-primary{background:linear-gradient(135deg,#0082c9,#00639b);color:#fff}.nc-btn-primary:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,130,201,.3)}.nc-btn-success{background:#10b981;color:#fff}.nc-btn-danger{background:#ef4444;color:#fff}.nc-btn-secondary{background:#6b7280;color:#fff}.nc-btn:disabled{opacity:.5;cursor:not-allowed}.nc-webui{display:flex;align-items:center;gap:1rem;padding:1rem;background:linear-gradient(135deg,rgba(0,130,201,.1),rgba(0,99,155,.1));border-radius:12px;margin-top:1rem}.nc-webui-icon{font-size:2rem}.nc-webui-info{flex:1}.nc-webui-url{font-family:monospace;color:#0082c9}.nc-not-installed{text-align:center;padding:3rem}.nc-not-installed h3{margin-bottom:1rem;color:#333}.nc-not-installed p{color:#666;margin-bottom:1.5rem}.nc-features{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem;margin:1.5rem 0;text-align:left}.nc-feature{padding:.75rem;background:#f0f9ff;border-radius:8px;font-size:.9rem}'; +var callStatus = rpc.declare({ + object: 'luci.nextcloud', + method: 'status', + expect: {} +}); + +var callGetConfig = rpc.declare({ + object: 'luci.nextcloud', + method: 'get_config', + expect: {} +}); + +var callInstall = rpc.declare({ + object: 'luci.nextcloud', + method: 'install', + expect: {} +}); + +var callStart = rpc.declare({ + object: 'luci.nextcloud', + method: 'start', + expect: {} +}); + +var callStop = rpc.declare({ + object: 'luci.nextcloud', + method: 'stop', + expect: {} +}); + +var callRestart = rpc.declare({ + object: 'luci.nextcloud', + method: 'restart', + expect: {} +}); + +var callUpdate = rpc.declare({ + object: 'luci.nextcloud', + method: 'update', + expect: {} +}); + +var callBackup = rpc.declare({ + object: 'luci.nextcloud', + method: 'backup', + params: ['name'], + expect: {} +}); + +var callRestore = rpc.declare({ + object: 'luci.nextcloud', + method: 'restore', + params: ['name'], + expect: {} +}); + +var callListBackups = rpc.declare({ + object: 'luci.nextcloud', + method: 'list_backups', + expect: { backups: [] } +}); + +var callSSLEnable = rpc.declare({ + object: 'luci.nextcloud', + method: 'ssl_enable', + params: ['domain'], + expect: {} +}); + +var callSSLDisable = rpc.declare({ + object: 'luci.nextcloud', + method: 'ssl_disable', + expect: {} +}); + +var callLogs = rpc.declare({ + object: 'luci.nextcloud', + method: 'logs', + expect: {} +}); + +// ============================================================================ +// Helpers +// ============================================================================ + +function fmtDate(timestamp) { + if (!timestamp) return '-'; + var d = new Date(timestamp * 1000); + return d.toLocaleDateString() + ' ' + d.toLocaleTimeString().slice(0, 5); +} + +function fmtRelative(timestamp) { + if (!timestamp) return '-'; + var d = new Date(timestamp * 1000); + var now = new Date(); + var diff = Math.floor((now - d) / 1000); + if (diff < 60) return diff + 's ago'; + if (diff < 3600) return Math.floor(diff / 60) + 'm ago'; + if (diff < 86400) return Math.floor(diff / 3600) + 'h ago'; + return Math.floor(diff / 86400) + 'd ago'; +} + +// ============================================================================ +// Main View +// ============================================================================ return view.extend({ - pollActive: true, + status: {}, + config: {}, + backups: [], + currentTab: 'overview', load: function() { - return callStatus(); - }, - - startPolling: function() { - var self = this; - this.pollActive = true; - poll.add(L.bind(function() { - if (!this.pollActive) return Promise.resolve(); - return callStatus().then(L.bind(function(status) { - this.updateStatus(status); - }, this)); - }, this), 5); - }, - - updateStatus: function(status) { - var badge = document.querySelector('.nc-status'); - var statusText = document.querySelector('.nc-status-text'); - - if (badge && statusText) { - badge.className = 'nc-status ' + (status.running ? 'running' : 'stopped'); - statusText.textContent = status.running ? _('Running') : _('Stopped'); - } - }, - - handleInstall: function() { - var self = this; - ui.showModal(_('Installing Nextcloud'), [ - E('p', { 'class': 'spinning' }, _('Installing Nextcloud. This may take several minutes...')) + return Promise.all([ + callStatus(), + callGetConfig(), + callListBackups().catch(function() { return { backups: [] }; }) ]); - callInstall().then(function(r) { - ui.hideModal(); - if (r.success) { - ui.addNotification(null, E('p', r.message || _('Installation started'))); - self.startPolling(); - } else { - ui.addNotification(null, E('p', _('Failed: ') + (r.error || 'Unknown error')), 'error'); - } - }).catch(function(e) { - ui.hideModal(); - ui.addNotification(null, E('p', _('Error: ') + e.message), 'error'); + }, + + render: function(data) { + var self = this; + this.status = data[0] || {}; + this.config = data[1] || {}; + this.backups = (data[2] || {}).backups || []; + + // Not installed - show install view + if (!this.status.installed) { + return this.renderInstallView(); + } + + // Tab navigation + var tabs = [ + { id: 'overview', label: 'Overview', icon: 'đŸŽ›ī¸' }, + { id: 'backups', label: 'Backups', icon: '💾' }, + { id: 'ssl', label: 'SSL', icon: '🔒' }, + { id: 'logs', label: 'Logs', icon: '📜' } + ]; + + var content = [ + // Header + E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;' }, [ + E('div', {}, [ + E('h1', { 'style': 'font-size:28px;font-weight:700;margin:0;display:flex;align-items:center;gap:12px;' }, [ + 'â˜ī¸ Nextcloud' + ]), + E('p', { 'style': 'color:var(--kiss-muted);margin:6px 0 0;' }, 'Self-hosted file sync and collaboration platform') + ]), + E('div', { 'style': 'display:flex;gap:8px;' }, [ + KissTheme.badge(this.status.version || 'N/A', 'blue'), + KissTheme.badge(this.status.running ? 'Running' : 'Stopped', + this.status.running ? 'green' : 'red') + ]) + ]), + + // Tab Navigation + E('div', { 'class': 'kiss-tabs', 'style': 'margin-bottom:20px;' }, + tabs.map(function(tab) { + return E('button', { + 'class': 'kiss-tab' + (self.currentTab === tab.id ? ' kiss-tab-active' : ''), + 'data-tab': tab.id, + 'click': function() { self.switchTab(tab.id); } + }, [tab.icon + ' ' + tab.label]); + }) + ), + + // Tab Content + E('div', { 'id': 'tab-content' }, this.renderTabContent()) + ]; + + poll.add(L.bind(this.refresh, this), 10); + return KissTheme.wrap(content, 'admin/secubox/services/nextcloud'); + }, + + switchTab: function(tabId) { + this.currentTab = tabId; + var tabContent = document.getElementById('tab-content'); + if (tabContent) { + dom.content(tabContent, this.renderTabContent()); + } + document.querySelectorAll('.kiss-tab').forEach(function(btn) { + btn.classList.toggle('kiss-tab-active', btn.dataset.tab === tabId); }); }, - handleStart: function() { - ui.showModal(_('Starting...'), [E('p', { 'class': 'spinning' }, _('Starting Nextcloud...'))]); - callStart().then(function(r) { - ui.hideModal(); - if (r.success) ui.addNotification(null, E('p', _('Nextcloud started'))); - else ui.addNotification(null, E('p', _('Failed to start')), 'error'); - }).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); }); + renderTabContent: function() { + switch (this.currentTab) { + case 'backups': return this.renderBackupsTab(); + case 'ssl': return this.renderSSLTab(); + case 'logs': return this.renderLogsTab(); + default: return this.renderOverviewTab(); + } }, - handleStop: function() { - ui.showModal(_('Stopping...'), [E('p', { 'class': 'spinning' }, _('Stopping Nextcloud...'))]); - callStop().then(function(r) { - ui.hideModal(); - if (r.success) ui.addNotification(null, E('p', _('Nextcloud stopped'))); - else ui.addNotification(null, E('p', _('Failed to stop')), 'error'); - }).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); }); - }, + // ======================================================================== + // Install View + // ======================================================================== - handleRestart: function() { - ui.showModal(_('Restarting...'), [E('p', { 'class': 'spinning' }, _('Restarting Nextcloud...'))]); - callRestart().then(function(r) { - ui.hideModal(); - if (r.success) ui.addNotification(null, E('p', _('Nextcloud restarted'))); - else ui.addNotification(null, E('p', _('Failed to restart')), 'error'); - }).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); }); - }, - - render: function(status) { + renderInstallView: function() { var self = this; - if (!document.getElementById('nc-styles')) { - var s = document.createElement('style'); - s.id = 'nc-styles'; - s.textContent = css; - document.head.appendChild(s); - } + var content = [ + E('div', { 'style': 'text-align:center;padding:60px 20px;' }, [ + E('div', { 'style': 'font-size:80px;margin-bottom:24px;' }, 'â˜ī¸'), + E('h1', { 'style': 'font-size:32px;margin:0 0 12px;' }, 'Nextcloud'), + E('p', { 'style': 'color:var(--kiss-muted);font-size:18px;max-width:500px;margin:0 auto 32px;' }, + 'Self-hosted productivity platform with file sync, calendar, contacts, and more.'), - // Not installed view - if (!status.installed || !status.docker_available) { - var content = E('div', { 'class': 'nc-container' }, [ - E('div', { 'class': 'nc-header' }, [ - E('h2', {}, ['\u2601\ufe0f ', _('Nextcloud')]), - E('div', { 'class': 'nc-status stopped' }, [ - E('span', { 'class': 'nc-dot' }), - E('span', { 'class': 'nc-status-text' }, _('Not Installed')) + E('div', { 'style': 'display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:16px;max-width:600px;margin:0 auto 40px;' }, [ + this.featureCard('📁', 'File Sync'), + this.featureCard('📅', 'Calendar'), + this.featureCard('đŸ‘Ĩ', 'Contacts'), + this.featureCard('📝', 'Documents'), + this.featureCard('📷', 'Photos'), + this.featureCard('🔐', 'E2E Encryption') + ]), + + E('div', { 'style': 'background:var(--kiss-bg2);border-radius:12px;padding:20px;max-width:500px;margin:0 auto 32px;text-align:left;' }, [ + E('div', { 'style': 'font-weight:600;margin-bottom:12px;' }, 'đŸ“Ļ What will be installed:'), + E('ul', { 'style': 'margin:0;padding-left:20px;color:var(--kiss-muted);font-size:14px;' }, [ + E('li', {}, 'Debian 12 LXC container'), + E('li', {}, 'Nextcloud with PHP 8.2'), + E('li', {}, 'MariaDB database'), + E('li', {}, 'Redis caching'), + E('li', {}, 'Nginx web server') ]) ]), - E('div', { 'class': 'nc-card' }, [ - E('div', { 'class': 'nc-not-installed' }, [ - E('div', { 'style': 'font-size:4rem;margin-bottom:1rem' }, '\u2601\ufe0f'), - E('h3', {}, _('Nextcloud')), - E('p', {}, _('Self-hosted productivity platform with file sync, calendar, contacts, and more.')), - E('div', { 'class': 'nc-features' }, [ - E('div', { 'class': 'nc-feature' }, '\ud83d\udcc1 ' + _('File Sync')), - E('div', { 'class': 'nc-feature' }, '\ud83d\udcc5 ' + _('Calendar')), - E('div', { 'class': 'nc-feature' }, '\ud83d\udc65 ' + _('Contacts')), - E('div', { 'class': 'nc-feature' }, '\ud83d\udcdd ' + _('Documents')), - E('div', { 'class': 'nc-feature' }, '\ud83d\udcf7 ' + _('Photos')), - E('div', { 'class': 'nc-feature' }, '\ud83d\udd12 ' + _('E2E Encryption')) - ]), - !status.docker_available ? E('div', { 'style': 'color:#ef4444;margin-bottom:1rem' }, _('Docker is required but not available')) : '', - E('button', { - 'class': 'nc-btn nc-btn-primary', - 'click': ui.createHandlerFn(this, 'handleInstall'), - 'disabled': !status.docker_available - }, _('Install Nextcloud')) - ]) + + E('button', { + 'class': 'kiss-btn kiss-btn-green', + 'style': 'font-size:18px;padding:16px 48px;', + 'click': function() { self.handleInstall(); } + }, '🚀 Install Nextcloud') + ]) + ]; + + return KissTheme.wrap(content, 'admin/secubox/services/nextcloud'); + }, + + featureCard: function(icon, label) { + return E('div', { 'style': 'background:var(--kiss-bg2);border-radius:10px;padding:16px;text-align:center;' }, [ + E('div', { 'style': 'font-size:28px;margin-bottom:8px;' }, icon), + E('div', { 'style': 'font-size:13px;font-weight:500;' }, label) + ]); + }, + + // ======================================================================== + // Overview Tab + // ======================================================================== + + renderOverviewTab: function() { + var self = this; + var s = this.status; + + return E('div', {}, [ + // Stats Grid + E('div', { 'class': 'kiss-grid kiss-grid-4', 'style': 'margin-bottom:24px;' }, [ + KissTheme.stat(s.running ? 'Online' : 'Offline', 'Status', s.running ? 'var(--kiss-green)' : 'var(--kiss-red)'), + KissTheme.stat(s.version || 'N/A', 'Version', 'var(--kiss-blue)'), + KissTheme.stat(s.user_count || 0, 'Users', 'var(--kiss-purple)'), + KissTheme.stat(s.disk_used || '0B', 'Storage', 'var(--kiss-cyan)') + ]), + + // Quick Actions + KissTheme.card([ + E('span', {}, '⚡ Quick Actions') + ], E('div', { 'style': 'display:flex;gap:12px;flex-wrap:wrap;' }, [ + E('button', { + 'class': 'kiss-btn ' + (s.running ? 'kiss-btn-red' : 'kiss-btn-green'), + 'click': function() { self.handleToggle(); } + }, s.running ? ['âšī¸ ', 'Stop'] : ['â–ļī¸ ', 'Start']), + E('button', { + 'class': 'kiss-btn', + 'click': function() { self.handleRestart(); }, + 'disabled': !s.running + }, ['🔄 ', 'Restart']), + s.web_accessible ? E('a', { + 'href': s.web_url, + 'target': '_blank', + 'class': 'kiss-btn kiss-btn-blue' + }, ['🌐 ', 'Open Nextcloud']) : null, + E('button', { + 'class': 'kiss-btn', + 'click': function() { self.handleUpdate(); } + }, ['âŦ†ī¸ ', 'Update']), + E('button', { + 'class': 'kiss-btn', + 'click': function() { self.handleQuickBackup(); } + }, ['💾 ', 'Backup Now']) + ].filter(Boolean))), + + // Service Info + KissTheme.card([ + E('span', {}, 'â„šī¸ Service Information') + ], E('div', { 'class': 'kiss-grid kiss-grid-2', 'style': 'gap:16px;' }, [ + E('div', { 'style': 'display:flex;flex-direction:column;gap:12px;' }, [ + this.infoRow('Container', s.container_name || 'nextcloud'), + this.infoRow('HTTP Port', s.http_port || 8080), + this.infoRow('Domain', s.domain || 'cloud.local'), + this.infoRow('Data Path', s.data_path || '/srv/nextcloud') + ]), + E('div', { 'style': 'display:flex;flex-direction:column;gap:12px;' }, [ + this.infoRow('SSL', s.ssl_enabled ? '✅ Enabled' : '❌ Disabled'), + this.infoRow('SSL Domain', s.ssl_domain || '-'), + this.infoRow('Web Accessible', s.web_accessible ? '✅ Yes' : '❌ No'), + this.infoRow('Enabled', s.enabled ? '✅ Yes' : '❌ No') ]) + ])), + + // Web Access Card + s.running && s.web_url ? KissTheme.card([ + E('span', {}, '🌐 Web Interface') + ], E('div', { 'style': 'display:flex;align-items:center;gap:16px;' }, [ + E('div', { 'style': 'flex:1;' }, [ + E('div', { 'style': 'font-size:14px;color:var(--kiss-muted);margin-bottom:4px;' }, 'Access your Nextcloud at:'), + E('div', { 'style': 'font-family:monospace;font-size:16px;color:var(--kiss-cyan);' }, s.web_url) + ]), + E('a', { + 'href': s.web_url, + 'target': '_blank', + 'class': 'kiss-btn kiss-btn-green' + }, ['🔗 ', 'Open']) + ])) : null + ].filter(Boolean)); + }, + + infoRow: function(label, value) { + return E('div', { 'style': 'display:flex;justify-content:space-between;padding:8px;background:var(--kiss-bg2);border-radius:6px;' }, [ + E('span', { 'style': 'color:var(--kiss-muted);' }, label), + E('span', { 'style': 'font-weight:500;' }, String(value)) + ]); + }, + + // ======================================================================== + // Backups Tab + // ======================================================================== + + renderBackupsTab: function() { + var self = this; + + return E('div', {}, [ + // Create Backup + KissTheme.card([ + E('span', {}, '➕ Create Backup') + ], E('div', { 'style': 'display:flex;gap:12px;align-items:center;' }, [ + E('input', { + 'id': 'backup-name', + 'type': 'text', + 'placeholder': 'Backup name (optional)', + 'style': 'flex:1;padding:10px;background:var(--kiss-bg2);border:1px solid var(--kiss-line);border-radius:6px;color:var(--kiss-text);' + }), + E('button', { + 'class': 'kiss-btn kiss-btn-green', + 'click': function() { + var name = document.getElementById('backup-name')?.value || ''; + self.handleBackup(name); + } + }, ['💾 ', 'Create Backup']) + ])), + + // Backup List + KissTheme.card([ + E('span', {}, 'đŸ“Ļ Available Backups'), + E('span', { 'style': 'margin-left:auto;font-size:12px;color:var(--kiss-muted);' }, this.backups.length + ' backups') + ], E('div', { 'id': 'backups-list' }, this.renderBackupsList())) + ]); + }, + + renderBackupsList: function() { + var self = this; + + if (!this.backups.length) { + return E('div', { 'style': 'text-align:center;padding:40px;color:var(--kiss-muted);' }, [ + E('div', { 'style': 'font-size:48px;margin-bottom:12px;' }, '💾'), + E('div', { 'style': 'font-size:16px;' }, 'No backups yet'), + E('div', { 'style': 'font-size:12px;margin-top:8px;' }, 'Create a backup to protect your data') ]); - return KissTheme.wrap(content, 'admin/secubox/services/nextcloud'); } - // Installed view - this.startPolling(); + return E('table', { 'class': 'kiss-table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Name'), + E('th', {}, 'Size'), + E('th', {}, 'Date'), + E('th', {}, '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')) + ]); + })) + ]); + }, - var content = E('div', { 'class': 'nc-container' }, [ - E('div', { 'class': 'nc-header' }, [ - E('h2', {}, ['\u2601\ufe0f ', _('Nextcloud')]), - E('div', { 'class': 'nc-status ' + (status.running ? 'running' : 'stopped') }, [ - E('span', { 'class': 'nc-dot' }), - E('span', { 'class': 'nc-status-text' }, status.running ? _('Running') : _('Stopped')) - ]) - ]), + // ======================================================================== + // SSL Tab + // ======================================================================== - // Info Card - E('div', { 'class': 'nc-card' }, [ - E('div', { 'class': 'nc-card-title' }, ['\u2139\ufe0f ', _('Service Information')]), - E('div', { 'class': 'nc-info-grid' }, [ - E('div', { 'class': 'nc-info-item' }, [ - E('div', { 'class': 'nc-info-label' }, _('Port')), - E('div', { 'class': 'nc-info-value' }, status.port || '80') - ]), - E('div', { 'class': 'nc-info-item' }, [ - E('div', { 'class': 'nc-info-label' }, _('Admin User')), - E('div', { 'class': 'nc-info-value' }, status.admin_user || 'admin') - ]), - E('div', { 'class': 'nc-info-item' }, [ - E('div', { 'class': 'nc-info-label' }, _('Trusted Domains')), - E('div', { 'class': 'nc-info-value' }, status.trusted_domains || 'cloud.local') - ]), - E('div', { 'class': 'nc-info-item' }, [ - E('div', { 'class': 'nc-info-label' }, _('Data Path')), - E('div', { 'class': 'nc-info-value' }, status.data_path || '/srv/nextcloud') - ]) + renderSSLTab: function() { + var self = this; + var s = this.status; + + return E('div', {}, [ + // SSL Status + KissTheme.card([ + E('span', {}, '🔒 SSL Status') + ], E('div', { 'style': 'display:flex;align-items:center;gap:16px;' }, [ + E('div', { 'style': 'font-size:48px;' }, s.ssl_enabled ? '🔐' : '🔓'), + E('div', { 'style': 'flex:1;' }, [ + E('div', { 'style': 'font-size:20px;font-weight:600;' }, s.ssl_enabled ? 'SSL Enabled' : 'SSL Disabled'), + s.ssl_enabled && s.ssl_domain ? E('div', { 'style': 'color:var(--kiss-muted);margin-top:4px;' }, 'Domain: ' + s.ssl_domain) : null ]), + s.ssl_enabled ? E('button', { + 'class': 'kiss-btn kiss-btn-red', + 'click': function() { self.handleSSLDisable(); } + }, '🔓 Disable SSL') : null + ].filter(Boolean))), - // Web UI Link - status.running && status.web_accessible ? E('div', { 'class': 'nc-webui' }, [ - E('div', { 'class': 'nc-webui-icon' }, '\ud83c\udf10'), - E('div', { 'class': 'nc-webui-info' }, [ - E('div', { 'style': 'font-weight:600' }, _('Web Interface')), - E('div', { 'class': 'nc-webui-url' }, status.web_url) - ]), - E('a', { - 'href': status.web_url, - 'target': '_blank', - 'class': 'nc-btn nc-btn-primary' - }, _('Open')) - ]) : '' - ]), - - // Actions Card - E('div', { 'class': 'nc-card' }, [ - E('div', { 'class': 'nc-card-title' }, ['\u26a1 ', _('Actions')]), - E('div', { 'class': 'nc-actions' }, [ + // Enable SSL Form + !s.ssl_enabled ? KissTheme.card([ + E('span', {}, '🌐 Enable SSL via HAProxy') + ], E('div', {}, [ + E('p', { 'style': 'color:var(--kiss-muted);margin-bottom:16px;' }, + 'Configure HTTPS access with automatic Let\'s Encrypt certificates via HAProxy.'), + E('div', { 'style': 'display:flex;gap:12px;align-items:center;' }, [ + E('input', { + 'id': 'ssl-domain', + 'type': 'text', + 'placeholder': 'cloud.example.com', + 'style': 'flex:1;padding:12px;background:var(--kiss-bg2);border:1px solid var(--kiss-line);border-radius:6px;color:var(--kiss-text);font-size:14px;' + }), E('button', { - 'class': 'nc-btn nc-btn-success', - 'click': ui.createHandlerFn(this, 'handleStart'), - 'disabled': status.running - }, _('Start')), - E('button', { - 'class': 'nc-btn nc-btn-danger', - 'click': ui.createHandlerFn(this, 'handleStop'), - 'disabled': !status.running - }, _('Stop')), - E('button', { - 'class': 'nc-btn nc-btn-secondary', - 'click': ui.createHandlerFn(this, 'handleRestart'), - 'disabled': !status.running - }, _('Restart')), - E('a', { - 'href': L.url('admin', 'secubox', 'services', 'nextcloud', 'settings'), - 'class': 'nc-btn nc-btn-secondary' - }, _('Settings')) + 'class': 'kiss-btn kiss-btn-green', + 'click': function() { + var domain = document.getElementById('ssl-domain')?.value; + if (domain) self.handleSSLEnable(domain); + else ui.addNotification(null, E('p', 'Enter a domain name'), 'warning'); + } + }, ['🔐 ', 'Enable SSL']) + ]), + E('div', { 'style': 'margin-top:16px;padding:12px;background:var(--kiss-bg2);border-radius:6px;' }, [ + E('div', { 'style': 'font-weight:600;margin-bottom:8px;color:var(--kiss-yellow);' }, 'âš ī¸ Prerequisites:'), + E('ul', { 'style': 'margin:0;padding-left:20px;color:var(--kiss-muted);font-size:13px;' }, [ + E('li', {}, 'Domain must point to this server\'s public IP'), + E('li', {}, 'Port 80 and 443 must be accessible'), + E('li', {}, 'HAProxy must be installed and running') + ]) ]) + ])) : null + ].filter(Boolean)); + }, + + // ======================================================================== + // Logs Tab + // ======================================================================== + + renderLogsTab: function() { + var self = this; + + return E('div', {}, [ + KissTheme.card([ + E('span', {}, '📜 Installation/Operation Logs'), + E('button', { + 'class': 'kiss-btn', + 'style': 'margin-left:auto;padding:4px 10px;font-size:11px;', + 'click': function() { self.refreshLogs(); } + }, '🔄 Refresh') + ], E('pre', { + 'id': 'logs-content', + 'style': 'background:#0a0a0a;color:#0f0;padding:16px;border-radius:8px;font-size:11px;height:400px;overflow-y:auto;margin:0;font-family:monospace;white-space:pre-wrap;' + }, '(Loading logs...)')) + ]); + }, + + // ======================================================================== + // Action Handlers + // ======================================================================== + + handleInstall: function() { + var self = this; + ui.showModal('Installing Nextcloud', [ + E('div', { 'style': 'text-align:center;padding:20px;' }, [ + E('div', { 'class': 'spinning', 'style': 'font-size:48px;' }, 'âŗ'), + E('p', { 'style': 'margin-top:16px;' }, 'Installing Nextcloud LXC container...'), + E('p', { 'style': 'color:var(--kiss-muted);font-size:13px;' }, 'This may take several minutes. Please wait.') ]) ]); - return KissTheme.wrap(content, 'admin/secubox/services/nextcloud'); + callInstall().then(function(r) { + ui.hideModal(); + if (r.success) { + ui.addNotification(null, E('p', r.message || 'Installation started'), 'info'); + } else { + ui.addNotification(null, E('p', 'Failed: ' + (r.error || 'Unknown error')), 'error'); + } + }).catch(function(e) { + ui.hideModal(); + ui.addNotification(null, E('p', 'Error: ' + e.message), 'error'); + }); + }, + + handleToggle: function() { + var self = this; + var running = this.status.running; + + ui.showModal(running ? 'Stopping...' : 'Starting...', [ + E('p', { 'class': 'spinning' }, (running ? 'Stopping' : 'Starting') + ' Nextcloud...') + ]); + + var fn = running ? callStop : callStart; + fn().then(function(r) { + ui.hideModal(); + if (r.success) { + ui.addNotification(null, E('p', running ? 'Nextcloud stopped' : 'Nextcloud started'), 'info'); + self.refresh(); + } 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'); + }); + }, + + handleRestart: function() { + var self = this; + ui.showModal('Restarting...', [ + E('p', { 'class': 'spinning' }, 'Restarting Nextcloud...') + ]); + + callRestart().then(function(r) { + ui.hideModal(); + if (r.success) { + ui.addNotification(null, E('p', 'Nextcloud restarted'), 'info'); + self.refresh(); + } 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'); + }); + }, + + handleUpdate: function() { + var self = this; + ui.showModal('Updating Nextcloud', [ + E('div', { 'style': 'text-align:center;padding:20px;' }, [ + E('div', { 'class': 'spinning', 'style': 'font-size:48px;' }, 'âŦ†ī¸'), + E('p', { 'style': 'margin-top:16px;' }, 'Updating Nextcloud...'), + E('p', { 'style': 'color:var(--kiss-muted);font-size:13px;' }, 'This may take a few minutes.') + ]) + ]); + + callUpdate().then(function(r) { + ui.hideModal(); + if (r.success) { + ui.addNotification(null, E('p', r.message || 'Update started'), 'info'); + } 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'); + }); + }, + + handleQuickBackup: function() { + this.handleBackup(''); + }, + + handleBackup: function(name) { + var self = this; + ui.showModal('Creating Backup', [ + E('p', { 'class': 'spinning' }, 'Creating backup...') + ]); + + callBackup(name || null).then(function(r) { + ui.hideModal(); + if (r.success) { + ui.addNotification(null, E('p', 'Backup created: ' + (r.backup_name || 'done')), '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'); + }); + }, + + handleRestore: function(name) { + var self = this; + if (!confirm('Restore from backup "' + name + '"? This will stop Nextcloud and may take several minutes.')) { + return; + } + + ui.showModal('Restoring Backup', [ + E('div', { 'style': 'text-align:center;padding:20px;' }, [ + E('div', { 'class': 'spinning', 'style': 'font-size:48px;' }, 'âŦ‡ī¸'), + E('p', { 'style': 'margin-top:16px;' }, 'Restoring from ' + name + '...'), + E('p', { 'style': 'color:var(--kiss-muted);font-size:13px;' }, 'This may take several minutes.') + ]) + ]); + + callRestore(name).then(function(r) { + ui.hideModal(); + if (r.success) { + ui.addNotification(null, E('p', r.message || 'Restore started'), 'info'); + self.refresh(); + } 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'); + }); + }, + + handleSSLEnable: function(domain) { + var self = this; + ui.showModal('Enabling SSL', [ + E('p', { 'class': 'spinning' }, 'Configuring SSL for ' + domain + '...') + ]); + + callSSLEnable(domain).then(function(r) { + ui.hideModal(); + if (r.success) { + ui.addNotification(null, E('p', r.message || 'SSL enabled'), 'info'); + self.refresh(); + } 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'); + }); + }, + + handleSSLDisable: function() { + var self = this; + if (!confirm('Disable SSL? HTTPS access will no longer work.')) { + return; + } + + callSSLDisable().then(function(r) { + if (r.success) { + ui.addNotification(null, E('p', 'SSL disabled'), 'info'); + self.refresh(); + } else { + ui.addNotification(null, E('p', 'Failed: ' + (r.error || 'Unknown')), 'error'); + } + }); + }, + + refreshBackups: function() { + var self = this; + callListBackups().then(function(data) { + self.backups = (data || {}).backups || []; + var container = document.getElementById('backups-list'); + if (container) dom.content(container, self.renderBackupsList()); + }); + }, + + refreshLogs: function() { + callLogs().then(function(data) { + var logs = (data.logs || '').replace(/\|/g, '\n'); + var el = document.getElementById('logs-content'); + if (el) { + el.textContent = logs || '(No logs available)'; + el.scrollTop = el.scrollHeight; + } + }); + }, + + // ======================================================================== + // Refresh + // ======================================================================== + + refresh: function() { + var self = this; + return Promise.all([ + callStatus(), + callListBackups().catch(function() { return { backups: [] }; }) + ]).then(function(data) { + self.status = data[0] || {}; + self.backups = (data[1] || {}).backups || []; + + // Update tab content + var tabContent = document.getElementById('tab-content'); + if (tabContent) { + dom.content(tabContent, self.renderTabContent()); + } + + // Refresh logs if on logs tab + if (self.currentTab === 'logs') { + self.refreshLogs(); + } + }); }, handleSaveApply: null, diff --git a/package/secubox/luci-app-nextcloud/htdocs/luci-static/resources/view/nextcloud/settings.js b/package/secubox/luci-app-nextcloud/htdocs/luci-static/resources/view/nextcloud/settings.js index 2e396561..b61c74dc 100644 --- a/package/secubox/luci-app-nextcloud/htdocs/luci-static/resources/view/nextcloud/settings.js +++ b/package/secubox/luci-app-nextcloud/htdocs/luci-static/resources/view/nextcloud/settings.js @@ -13,59 +13,128 @@ return view.extend({ var m, s, o; m = new form.Map('nextcloud', _('Nextcloud Settings'), - _('Configure Nextcloud settings. Changes require service restart to take effect.')); + _('Configure Nextcloud LXC container settings. Changes require service restart to take effect.')); - s = m.section(form.TypedSection, 'nextcloud', _('General Settings')); - s.anonymous = true; + // Main Settings + s = m.section(form.NamedSection, 'main', 'nextcloud', _('General Settings')); + s.anonymous = false; s.addremove = false; o = s.option(form.Flag, 'enabled', _('Enabled'), - _('Enable Nextcloud')); + _('Enable Nextcloud service')); o.default = '0'; o.rmempty = false; - o = s.option(form.Value, 'port', _('Web UI Port'), + o = s.option(form.Value, 'http_port', _('HTTP Port'), _('Port for the Nextcloud web interface')); o.datatype = 'port'; - o.default = '80'; - o.placeholder = '80'; + o.default = '8080'; + o.placeholder = '8080'; o = s.option(form.Value, 'data_path', _('Data Path'), - _('Path to store Nextcloud data')); + _('Path to store Nextcloud data and backups')); o.default = '/srv/nextcloud'; o.placeholder = '/srv/nextcloud'; + o = s.option(form.Value, 'domain', _('Domain'), + _('Primary domain name for Nextcloud')); + o.default = 'cloud.local'; + o.placeholder = 'cloud.local'; + o = s.option(form.Value, 'admin_user', _('Admin Username'), - _('Administrator username for initial setup')); + _('Administrator username')); o.default = 'admin'; o.placeholder = 'admin'; o = s.option(form.Value, 'admin_password', _('Admin Password'), - _('Administrator password for initial setup. Required for first install.')); + _('Administrator password (only used during initial setup)')); o.password = true; o.placeholder = _('Enter password'); - o = s.option(form.Value, 'trusted_domains', _('Trusted Domains'), - _('Comma-separated list of trusted domains (e.g., cloud.example.com,192.168.1.1)')); - o.default = 'cloud.local'; - o.placeholder = 'cloud.local'; + o = s.option(form.Value, 'memory_limit', _('PHP Memory Limit'), + _('Memory limit for PHP (e.g., 512M, 1G, 2G)')); + o.default = '1G'; + o.placeholder = '1G'; - o = s.option(form.Value, 'timezone', _('Timezone'), - _('Timezone for the container')); - o.default = 'UTC'; - o.placeholder = 'UTC'; + o = s.option(form.Value, 'upload_max', _('Max Upload Size'), + _('Maximum file upload size (e.g., 512M, 1G)')); + o.default = '512M'; + o.placeholder = '512M'; - o = s.option(form.Value, 'image', _('Docker Image'), - _('Docker image to use')); - o.default = 'nextcloud:latest'; - o.placeholder = 'nextcloud:latest'; + o = s.option(form.Value, 'trusted_proxies', _('Trusted Proxies'), + _('IP addresses of trusted reverse proxies')); + o.default = '127.0.0.1'; + o.placeholder = '127.0.0.1'; + + // Redis Cache Settings + s = m.section(form.NamedSection, 'redis', 'cache', _('Redis Cache')); + s.anonymous = false; + s.addremove = false; + + o = s.option(form.Flag, 'enabled', _('Enable Redis'), + _('Enable Redis caching for improved performance')); + o.default = '1'; + o.rmempty = false; + + o = s.option(form.Value, 'memory', _('Redis Memory'), + _('Maximum memory for Redis cache')); + o.default = '128M'; + o.placeholder = '128M'; + + // SSL Settings + s = m.section(form.NamedSection, 'ssl', 'haproxy', _('SSL / HAProxy')); + s.anonymous = false; + s.addremove = false; + + o = s.option(form.Flag, 'enabled', _('Enable SSL'), + _('Enable HTTPS via HAProxy')); + o.default = '0'; + o.rmempty = false; + + o = s.option(form.Value, 'domain', _('SSL Domain'), + _('Domain name for SSL certificate')); + o.placeholder = 'cloud.example.com'; + o.depends('enabled', '1'); + + o = s.option(form.Flag, 'acme', _('Auto SSL (ACME)'), + _('Automatically obtain SSL certificate from Let\'s Encrypt')); + o.default = '1'; + o.rmempty = false; + o.depends('enabled', '1'); + + // Backup Settings + s = m.section(form.NamedSection, 'backup', 'backup', _('Backup Settings')); + s.anonymous = false; + s.addremove = false; + + o = s.option(form.Flag, 'enabled', _('Enable Auto Backup'), + _('Enable automatic scheduled backups')); + o.default = '1'; + o.rmempty = false; + + o = s.option(form.ListValue, 'schedule', _('Backup Schedule'), + _('How often to create automatic backups')); + o.value('daily', _('Daily')); + o.value('weekly', _('Weekly')); + o.value('hourly', _('Hourly')); + o.default = 'daily'; + o.depends('enabled', '1'); + + o = s.option(form.Value, 'keep', _('Keep Backups'), + _('Number of backups to retain')); + o.datatype = 'uinteger'; + o.default = '7'; + o.placeholder = '7'; + o.depends('enabled', '1'); + + o = s.option(form.Value, 'path', _('Backup Path'), + _('Directory to store backups')); + o.default = '/srv/nextcloud/backups'; + o.placeholder = '/srv/nextcloud/backups'; + o.depends('enabled', '1'); return m.render().then(function(node) { return KissTheme.wrap(node, 'admin/secubox/services/nextcloud/settings'); }); - }, - - handleSaveApply: null, - handleSave: null, - handleReset: null + } }); 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 6ce68870..a5b7e4e0 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 @@ -1,47 +1,62 @@ #!/bin/sh -# RPCD backend for Nextcloud LuCI app +# RPCD backend for Nextcloud LuCI app (LXC version) +# Copyright (C) 2025 CyberMind.fr CONFIG="nextcloud" -CONTAINER="secbx-nextcloud" +LXC_NAME="nextcloud" +LXC_ROOTFS="/srv/lxc/nextcloud/rootfs" -uci_get() { uci -q get ${CONFIG}.main.$1; } -uci_set() { uci set ${CONFIG}.main.$1="$2" && uci commit ${CONFIG}; } +uci_get() { uci -q get ${CONFIG}.$1; } +uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; } + +# Check if LXC container is running +lxc_running() { + lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING" +} + +# Check if container is installed +lxc_installed() { + [ -d "$LXC_ROOTFS" ] && [ -f "/srv/lxc/$LXC_NAME/config" ] +} # Get service status get_status() { - local enabled=$(uci_get enabled) - local port=$(uci_get port) - local data_path=$(uci_get data_path) - local admin_user=$(uci_get admin_user) - local trusted_domains=$(uci_get trusted_domains) - local image=$(uci_get image) + local enabled=$(uci_get main.enabled) + local http_port=$(uci_get main.http_port) + local data_path=$(uci_get main.data_path) + local domain=$(uci_get main.domain) + local ssl_enabled=$(uci_get ssl.enabled) + local ssl_domain=$(uci_get ssl.domain) - # Check if Docker is available - local docker_available=0 - command -v docker >/dev/null 2>&1 && docker_available=1 - - # Check if container is running + # Container status local running=0 - local container_status="stopped" - if [ "$docker_available" = "1" ]; then - if docker ps --filter "name=$CONTAINER" --format "{{.Names}}" 2>/dev/null | grep -q "$CONTAINER"; then - running=1 - container_status="running" - elif docker ps -a --filter "name=$CONTAINER" --format "{{.Names}}" 2>/dev/null | grep -q "$CONTAINER"; then - container_status="stopped" - fi - fi - - # Check if installed (image exists) local installed=0 - if [ "$docker_available" = "1" ]; then - docker images --format "{{.Repository}}" 2>/dev/null | grep -q "nextcloud" && installed=1 + local version="" + local user_count=0 + local disk_used="0" + + lxc_installed && installed=1 + lxc_running && running=1 + + # Get Nextcloud info if running + if [ "$running" = "1" ]; then + version=$(lxc-attach -n "$LXC_NAME" -- su -s /bin/bash www-data -c "php /var/www/nextcloud/occ -V" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo "") + user_count=$(lxc-attach -n "$LXC_NAME" -- su -s /bin/bash www-data -c "php /var/www/nextcloud/occ user:list --output=json" 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | wc -l || echo 0) fi - # Check web UI accessibility + # Get disk usage + if [ -d "${data_path:-/srv/nextcloud}/data" ]; then + disk_used=$(du -sh "${data_path:-/srv/nextcloud}/data" 2>/dev/null | awk '{print $1}' || echo "0") + fi + + # Get LAN IP + local lan_ip + lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.255.1") + + # Check web accessibility local web_accessible=0 if [ "$running" = "1" ]; then - wget -q -O /dev/null --timeout=2 "http://127.0.0.1:${port:-80}/" 2>/dev/null && web_accessible=1 + wget -q -O /dev/null --timeout=2 "http://127.0.0.1:${http_port:-8080}/status.php" 2>/dev/null && web_accessible=1 fi cat </dev/null) + local http_port=$(echo "$input" | jsonfilter -e '@.http_port' 2>/dev/null) local data_path=$(echo "$input" | jsonfilter -e '@.data_path' 2>/dev/null) + local domain=$(echo "$input" | jsonfilter -e '@.domain' 2>/dev/null) local admin_user=$(echo "$input" | jsonfilter -e '@.admin_user' 2>/dev/null) - local admin_password=$(echo "$input" | jsonfilter -e '@.admin_password' 2>/dev/null) - local trusted_domains=$(echo "$input" | jsonfilter -e '@.trusted_domains' 2>/dev/null) - local timezone=$(echo "$input" | jsonfilter -e '@.timezone' 2>/dev/null) + local memory_limit=$(echo "$input" | jsonfilter -e '@.memory_limit' 2>/dev/null) + local upload_max=$(echo "$input" | jsonfilter -e '@.upload_max' 2>/dev/null) - [ -n "$port" ] && uci_set port "$port" - [ -n "$data_path" ] && uci_set data_path "$data_path" - [ -n "$admin_user" ] && uci_set admin_user "$admin_user" - [ -n "$admin_password" ] && uci_set admin_password "$admin_password" - [ -n "$trusted_domains" ] && uci_set trusted_domains "$trusted_domains" - [ -n "$timezone" ] && uci_set timezone "$timezone" + [ -n "$http_port" ] && uci_set main.http_port "$http_port" + [ -n "$data_path" ] && uci_set main.data_path "$data_path" + [ -n "$domain" ] && uci_set main.domain "$domain" + [ -n "$admin_user" ] && uci_set main.admin_user "$admin_user" + [ -n "$memory_limit" ] && uci_set main.memory_limit "$memory_limit" + [ -n "$upload_max" ] && uci_set main.upload_max "$upload_max" echo '{"success": true}' } @@ -122,8 +147,9 @@ do_install() { # Start service do_start() { if [ -x /etc/init.d/nextcloud ]; then + uci_set main.enabled '1' /etc/init.d/nextcloud start >/dev/null 2>&1 - uci_set enabled '1' + sleep 2 echo '{"success": true}' else echo '{"success": false, "error": "Service not installed"}' @@ -160,6 +186,99 @@ do_update() { fi } +# Create backup +do_backup() { + local input + read -r input + local name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) + + if command -v nextcloudctl >/dev/null 2>&1; then + local result + if [ -n "$name" ]; then + result=$(nextcloudctl backup "$name" 2>&1) + else + result=$(nextcloudctl backup 2>&1) + fi + echo "{\"success\": true, \"backup_name\": \"$result\"}" + else + echo '{"success": false, "error": "nextcloudctl not found"}' + fi +} + +# Restore backup +do_restore() { + local input + read -r input + local name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) + + if [ -z "$name" ]; then + echo '{"success": false, "error": "No backup name specified"}' + return + fi + + if command -v nextcloudctl >/dev/null 2>&1; then + nextcloudctl restore "$name" >/tmp/nextcloud-restore.log 2>&1 & + echo '{"success": true, "message": "Restore started in background"}' + else + echo '{"success": false, "error": "nextcloudctl not found"}' + fi +} + +# List backups +list_backups() { + local data_path=$(uci_get main.data_path) + local backup_dir="${data_path:-/srv/nextcloud}/backups" + local backups="[]" + + if [ -d "$backup_dir" ]; then + backups=$(ls -1 "$backup_dir"/*-db.sql 2>/dev/null | while read f; do + local name=$(basename "$f" -db.sql) + local data_file="$backup_dir/$name-data.tar.gz" + local size="N/A" + local timestamp=0 + + if [ -f "$data_file" ]; then + size=$(ls -lh "$data_file" | awk '{print $5}') + fi + timestamp=$(stat -c %Y "$f" 2>/dev/null || echo 0) + + printf '{"name":"%s","size":"%s","timestamp":%d},' "$name" "$size" "$timestamp" + done | sed 's/,$//' | sed 's/^/[/;s/$/]/') + fi + + [ -z "$backups" ] && backups="[]" + echo "{\"backups\": $backups}" +} + +# Enable SSL +do_ssl_enable() { + local input + read -r input + local domain=$(echo "$input" | jsonfilter -e '@.domain' 2>/dev/null) + + if [ -z "$domain" ]; then + echo '{"success": false, "error": "No domain specified"}' + return + fi + + if command -v nextcloudctl >/dev/null 2>&1; then + nextcloudctl ssl-enable "$domain" >/tmp/nextcloud-ssl.log 2>&1 + echo '{"success": true, "message": "SSL enabled for '$domain'"}' + else + echo '{"success": false, "error": "nextcloudctl not found"}' + fi +} + +# Disable SSL +do_ssl_disable() { + if command -v nextcloudctl >/dev/null 2>&1; then + nextcloudctl ssl-disable >/dev/null 2>&1 + echo '{"success": true}' + else + echo '{"success": false, "error": "nextcloudctl not found"}' + fi +} + # Run OCC command do_occ() { local input @@ -173,7 +292,8 @@ do_occ() { if command -v nextcloudctl >/dev/null 2>&1; then local output=$(nextcloudctl occ $cmd 2>&1) - echo "{\"success\": true, \"output\": \"$(echo "$output" | sed 's/"/\\"/g' | tr '\n' ' ')\"}" + local escaped=$(echo "$output" | sed 's/"/\\"/g' | tr '\n' ' ') + echo "{\"success\": true, \"output\": \"$escaped\"}" else echo '{"success": false, "error": "nextcloudctl not found"}' fi @@ -181,9 +301,10 @@ do_occ() { # Get logs get_logs() { - local lines=50 + local lines=100 local log_content="" + # Check install log if [ -f /tmp/nextcloud-install.log ]; then log_content=$(tail -n $lines /tmp/nextcloud-install.log 2>/dev/null | sed 's/"/\\"/g' | tr '\n' '|') fi @@ -197,12 +318,17 @@ list_methods() { { "status": {}, "get_config": {}, - "save_config": {"port": "string", "data_path": "string", "admin_user": "string", "admin_password": "string", "trusted_domains": "string", "timezone": "string"}, + "save_config": {"http_port": "string", "data_path": "string", "domain": "string", "admin_user": "string", "memory_limit": "string", "upload_max": "string"}, "install": {}, "start": {}, "stop": {}, "restart": {}, "update": {}, + "backup": {"name": "string"}, + "restore": {"name": "string"}, + "list_backups": {}, + "ssl_enable": {"domain": "string"}, + "ssl_disable": {}, "occ": {"command": "string"}, "logs": {} } @@ -216,17 +342,22 @@ case "$1" in ;; call) case "$2" in - status) get_status ;; - get_config) get_config ;; - save_config) save_config ;; - install) do_install ;; - start) do_start ;; - stop) do_stop ;; - restart) do_restart ;; - update) do_update ;; - occ) do_occ ;; - logs) get_logs ;; - *) echo '{"error": "Unknown method"}' ;; + status) get_status ;; + get_config) get_config ;; + save_config) save_config ;; + install) do_install ;; + start) do_start ;; + stop) do_stop ;; + restart) do_restart ;; + update) do_update ;; + backup) do_backup ;; + restore) do_restore ;; + list_backups) list_backups ;; + ssl_enable) do_ssl_enable ;; + ssl_disable) do_ssl_disable ;; + occ) do_occ ;; + logs) get_logs ;; + *) echo '{"error": "Unknown method"}' ;; esac ;; *) diff --git a/package/secubox/luci-app-nextcloud/root/usr/share/luci/menu.d/luci-app-nextcloud.json b/package/secubox/luci-app-nextcloud/root/usr/share/luci/menu.d/luci-app-nextcloud.json index 2acfcdb3..80199100 100644 --- a/package/secubox/luci-app-nextcloud/root/usr/share/luci/menu.d/luci-app-nextcloud.json +++ b/package/secubox/luci-app-nextcloud/root/usr/share/luci/menu.d/luci-app-nextcloud.json @@ -1,7 +1,7 @@ { - "admin/services/nextcloud": { + "admin/secubox/services/nextcloud": { "title": "Nextcloud", - "order": 55, + "order": 30, "action": { "type": "firstchild" }, @@ -9,7 +9,7 @@ "acl": ["luci-app-nextcloud"] } }, - "admin/services/nextcloud/overview": { + "admin/secubox/services/nextcloud/overview": { "title": "Overview", "order": 10, "action": { @@ -17,9 +17,9 @@ "path": "nextcloud/overview" } }, - "admin/services/nextcloud/settings": { + "admin/secubox/services/nextcloud/settings": { "title": "Settings", - "order": 90, + "order": 20, "action": { "type": "view", "path": "nextcloud/settings" 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 234048c9..916582ff 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 @@ -1,15 +1,27 @@ { "luci-app-nextcloud": { - "description": "Grant access to Nextcloud", + "description": "Grant access to Nextcloud LXC app", "read": { "ubus": { - "luci.nextcloud": ["status", "get_config", "logs"] + "luci.nextcloud": ["status", "get_config", "list_backups", "logs"] }, "uci": ["nextcloud"] }, "write": { "ubus": { - "luci.nextcloud": ["install", "start", "stop", "restart", "update", "save_config", "occ"] + "luci.nextcloud": [ + "install", + "start", + "stop", + "restart", + "update", + "save_config", + "backup", + "restore", + "ssl_enable", + "ssl_disable", + "occ" + ] }, "uci": ["nextcloud"] } diff --git a/package/secubox/secubox-app-nextcloud/Makefile b/package/secubox/secubox-app-nextcloud/Makefile index 1736dfea..870f0089 100644 --- a/package/secubox/secubox-app-nextcloud/Makefile +++ b/package/secubox/secubox-app-nextcloud/Makefile @@ -14,14 +14,14 @@ define Package/secubox-app-nextcloud CATEGORY:=Utilities PKGARCH:=all SUBMENU:=SecuBox Apps - TITLE:=SecuBox Nextcloud docker app - DEPENDS:=dockerd +docker +containerd + TITLE:=SecuBox Nextcloud LXC app + DEPENDS:=+lxc +lxc-common +tar +wget-ssl +jsonfilter +openssl-util +unzip +xz endef define Package/secubox-app-nextcloud/description -Installer, configuration, and service manager for running Nextcloud -inside Docker on SecuBox-powered OpenWrt systems. Self-hosted file -sync and share with calendar, contacts, and collaboration. +Nextcloud file sync and collaboration platform running in a Debian-based +LXC container with MariaDB, Redis, and Nginx. Features HAProxy SSL +integration, automated backups, and KISS LuCI dashboard. endef define Package/secubox-app-nextcloud/conffiles diff --git a/package/secubox/secubox-app-nextcloud/files/etc/config/nextcloud b/package/secubox/secubox-app-nextcloud/files/etc/config/nextcloud index 448b8642..cf19ea24 100644 --- a/package/secubox/secubox-app-nextcloud/files/etc/config/nextcloud +++ b/package/secubox/secubox-app-nextcloud/files/etc/config/nextcloud @@ -1,9 +1,31 @@ config nextcloud 'main' option enabled '0' - option image 'nextcloud:latest' option data_path '/srv/nextcloud' - option port '80' + option http_port '8080' + option domain 'cloud.local' option admin_user 'admin' option admin_password '' - option trusted_domains 'cloud.local' - option timezone 'UTC' + option memory_limit '1G' + option upload_max '512M' + option trusted_proxies '127.0.0.1' + +config database 'db' + option type 'mariadb' + option name 'nextcloud' + option user 'nextcloud' + option password '' + +config cache 'redis' + option enabled '1' + option memory '128M' + +config haproxy 'ssl' + option enabled '0' + option domain '' + option acme '1' + +config backup 'backup' + option enabled '1' + option schedule 'daily' + option keep '7' + option path '/srv/nextcloud/backups' diff --git a/package/secubox/secubox-app-nextcloud/files/etc/init.d/nextcloud b/package/secubox/secubox-app-nextcloud/files/etc/init.d/nextcloud index 72bda81b..ea9f4626 100755 --- a/package/secubox/secubox-app-nextcloud/files/etc/init.d/nextcloud +++ b/package/secubox/secubox-app-nextcloud/files/etc/init.d/nextcloud @@ -1,23 +1,40 @@ #!/bin/sh /etc/rc.common +# SecuBox Nextcloud LXC Service +# Copyright (C) 2025 CyberMind.fr -START=95 +START=90 STOP=10 USE_PROCD=1 -SERVICE_BIN="/usr/sbin/nextcloudctl" +PROG=/usr/sbin/nextcloudctl +CONFIG=nextcloud start_service() { + local enabled + + config_load "$CONFIG" + config_get enabled main enabled '0' + + [ "$enabled" = "1" ] || return 0 + procd_open_instance - procd_set_param command "$SERVICE_BIN" service-run - procd_set_param respawn 2000 5 5 + procd_set_param command "$PROG" service-run + procd_set_param respawn 3600 5 0 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param pidfile /var/run/nextcloud.pid procd_close_instance } stop_service() { - "$SERVICE_BIN" service-stop >/dev/null 2>&1 + "$PROG" service-stop } -restart_service() { +reload_service() { stop_service start_service } + +service_triggers() { + procd_add_reload_trigger "$CONFIG" +} diff --git a/package/secubox/secubox-app-nextcloud/files/usr/sbin/nextcloudctl b/package/secubox/secubox-app-nextcloud/files/usr/sbin/nextcloudctl index 67464188..9a3b4279 100644 --- a/package/secubox/secubox-app-nextcloud/files/usr/sbin/nextcloudctl +++ b/package/secubox/secubox-app-nextcloud/files/usr/sbin/nextcloudctl @@ -1,135 +1,1018 @@ #!/bin/sh -# SecuBox Nextcloud manager +# SecuBox Nextcloud Platform Controller +# Copyright (C) 2025 CyberMind.fr +# +# Manages Nextcloud in Debian-based LXC container with MariaDB & Redis CONFIG="nextcloud" -CONTAINER="secbx-nextcloud" -OPKG_UPDATED=0 +LXC_NAME="nextcloud" +NEXTCLOUD_VERSION="30.0.4" -usage() { - cat <<'USAGE' -Usage: nextcloudctl +# Paths +LXC_PATH="/srv/lxc" +LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs" +LXC_CONFIG="$LXC_PATH/$LXC_NAME/config" +DATA_PATH="/srv/nextcloud" +BACKUP_PATH="/srv/nextcloud/backups" -Commands: - install Install prerequisites, prepare directories, pull image - check Run prerequisite checks - update Pull new image and restart - status Show container status - logs Show container logs (use -f to follow) - occ Run Nextcloud OCC command (e.g., occ config:system:set trusted_domains 1 --value=example.com) - service-run Internal: run container via procd - service-stop Stop container -USAGE +# Logging +log_info() { echo "[INFO] $*"; logger -t nextcloud "$*"; } +log_error() { echo "[ERROR] $*" >&2; logger -t nextcloud -p err "$*"; } +log_debug() { [ "$DEBUG" = "1" ] && echo "[DEBUG] $*"; } + +# Helpers +require_root() { + [ "$(id -u)" -eq 0 ] || { + log_error "This command requires root privileges" + exit 1 + } } -require_root() { [ "$(id -u)" -eq 0 ]; } - -uci_get() { uci -q get ${CONFIG}.main.$1; } - -defaults() { - image="$(uci_get image || echo nextcloud:latest)" - data_path="$(uci_get data_path || echo /srv/nextcloud)" - port="$(uci_get port || echo 80)" - admin_user="$(uci_get admin_user || echo admin)" - admin_password="$(uci_get admin_password)" - trusted_domains="$(uci_get trusted_domains || echo cloud.local)" - timezone="$(uci_get timezone || echo UTC)" -} +has_lxc() { command -v lxc-start >/dev/null 2>&1; } ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; } -ensure_packages() { - for pkg in "$@"; do - if ! opkg status "$pkg" >/dev/null 2>&1; then - if [ "$OPKG_UPDATED" -eq 0 ]; then - opkg update || return 1 - OPKG_UPDATED=1 - fi - opkg install "$pkg" || return 1 +uci_get() { uci -q get ${CONFIG}.$1; } +uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; } + +# Generate random password +gen_password() { + head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 16 +} + +# Load configuration +load_config() { + http_port="$(uci_get main.http_port)" || http_port="8080" + data_path="$(uci_get main.data_path)" || data_path="/srv/nextcloud" + domain="$(uci_get main.domain)" || domain="cloud.local" + admin_user="$(uci_get main.admin_user)" || admin_user="admin" + admin_password="$(uci_get main.admin_password)" + memory_limit="$(uci_get main.memory_limit)" || memory_limit="1G" + upload_max="$(uci_get main.upload_max)" || upload_max="512M" + trusted_proxies="$(uci_get main.trusted_proxies)" || trusted_proxies="127.0.0.1" + + # Database settings + db_name="$(uci_get db.name)" || db_name="nextcloud" + db_user="$(uci_get db.user)" || db_user="nextcloud" + db_password="$(uci_get db.password)" + + # Redis settings + redis_enabled="$(uci_get redis.enabled)" || redis_enabled="1" + redis_memory="$(uci_get redis.memory)" || redis_memory="128M" + + # HAProxy/SSL settings + ssl_enabled="$(uci_get ssl.enabled)" || ssl_enabled="0" + ssl_domain="$(uci_get ssl.domain)" + ssl_acme="$(uci_get ssl.acme)" || ssl_acme="1" + + # Backup settings + backup_enabled="$(uci_get backup.enabled)" || backup_enabled="1" + backup_keep="$(uci_get backup.keep)" || backup_keep="7" + + DATA_PATH="$data_path" + BACKUP_PATH="$data_path/backups" + + ensure_dir "$data_path" + ensure_dir "$data_path/data" + ensure_dir "$data_path/config" + ensure_dir "$BACKUP_PATH" +} + +# Usage +usage() { + cat < [options] + +Commands: + install Create Debian LXC and install Nextcloud stack + uninstall Remove container (preserves data) + update Update Nextcloud to latest version + start Start Nextcloud service (via init) + stop Stop Nextcloud service (via init) + restart Restart Nextcloud service + status Show service status (JSON format) + logs [-f] Show container logs + shell Open shell in container + + occ Run Nextcloud OCC command + backup [name] Create backup of data and database + restore Restore from backup + list-backups List available backups + + ssl-enable Register with HAProxy for SSL + ssl-disable Remove HAProxy registration + + service-run Start service (used by init) + service-stop Stop service (used by init) + +Configuration: + /etc/config/nextcloud + +Data directory: + /srv/nextcloud + +EOF +} + +# Check prerequisites +lxc_check_prereqs() { + if ! has_lxc; then + log_error "LXC not installed. Install with: opkg install lxc lxc-common" + return 1 + fi + return 0 +} + +# Create LXC rootfs from Debian +lxc_create_rootfs() { + local rootfs="$LXC_ROOTFS" + local arch=$(uname -m) + + log_info "Creating Debian 12 rootfs for Nextcloud..." + + ensure_dir "$rootfs" + + # Use Debian mini rootfs from LXC images + case "$arch" in + x86_64) deb_arch="amd64" ;; + aarch64) deb_arch="arm64" ;; + armv7l) deb_arch="armhf" ;; + *) log_error "Unsupported architecture: $arch"; return 1 ;; + esac + + # Download Debian rootfs from LXC image server + local base_url="https://images.linuxcontainers.org/images/debian/bookworm/${deb_arch}/default" + local index_url="${base_url}/index.json" + local tmpdir="/tmp/debian-rootfs-$$" + mkdir -p "$tmpdir" + + log_info "Fetching latest Debian 12 rootfs index..." + local latest_path + latest_path=$(wget -q -O- "$index_url" 2>/dev/null | jsonfilter -e '@[*].path' 2>/dev/null | head -1) + + if [ -z "$latest_path" ]; then + # Fallback: try direct date-based path + local today=$(date +%Y%m%d) + latest_path="${today}_05:10" + fi + + local rootfs_url="https://images.linuxcontainers.org${latest_path}/rootfs.tar.xz" + local tmpfile="$tmpdir/rootfs.tar.xz" + + log_info "Downloading Debian rootfs..." + wget -q --show-progress -O "$tmpfile" "$rootfs_url" || { + # Fallback: try alternate URL format + rootfs_url="${base_url}/${latest_path}/rootfs.tar.xz" + wget -q --show-progress -O "$tmpfile" "$rootfs_url" || { + log_error "Failed to download Debian rootfs" + rm -rf "$tmpdir" + return 1 + } + } + + log_info "Extracting rootfs..." + tar -xJf "$tmpfile" -C "$rootfs" || { + log_error "Failed to extract rootfs" + rm -rf "$tmpdir" + return 1 + } + rm -rf "$tmpdir" + + # Setup resolv.conf + cp /etc/resolv.conf "$rootfs/etc/resolv.conf" 2>/dev/null || \ + echo "nameserver 1.1.1.1" > "$rootfs/etc/resolv.conf" + + # Create required directories + mkdir -p "$rootfs/opt" + mkdir -p "$rootfs/run" + mkdir -p "$rootfs/var/www/nextcloud" + + log_info "Rootfs created successfully" + return 0 +} + +# Install packages inside container +install_container_packages() { + local rootfs="$LXC_ROOTFS" + + log_info "Installing Nextcloud stack packages..." + + # Create install script + cat > "$rootfs/tmp/install-deps.sh" << 'SCRIPT' +#!/bin/bash +set -e + +export DEBIAN_FRONTEND=noninteractive + +# Update and install packages +apt-get update +apt-get install -y --no-install-recommends \ + nginx \ + mariadb-server \ + redis-server \ + php-fpm \ + php-gd \ + php-mysql \ + php-curl \ + php-mbstring \ + php-intl \ + php-gmp \ + php-bcmath \ + php-xml \ + php-zip \ + php-imagick \ + php-redis \ + php-apcu \ + php-ldap \ + php-smbclient \ + unzip \ + curl \ + wget \ + cron + +# Clean up +apt-get clean +rm -rf /var/lib/apt/lists/* + +touch /tmp/.deps-installed +SCRIPT + chmod +x "$rootfs/tmp/install-deps.sh" + + # Run via chroot (container not started yet) + log_info "Running package installation via chroot..." + chroot "$rootfs" /tmp/install-deps.sh || { + log_error "Failed to install packages" + return 1 + } + + rm -f "$rootfs/tmp/install-deps.sh" + log_info "Container packages installed" + return 0 +} + +# Download and install Nextcloud +install_nextcloud() { + local rootfs="$LXC_ROOTFS" + local nc_dir="$rootfs/var/www/nextcloud" + + log_info "Downloading Nextcloud ${NEXTCLOUD_VERSION}..." + + local nc_url="https://download.nextcloud.com/server/releases/nextcloud-${NEXTCLOUD_VERSION}.zip" + local tmpfile="/tmp/nextcloud.zip" + + wget -q --show-progress -O "$tmpfile" "$nc_url" || { + log_error "Failed to download Nextcloud" + return 1 + } + + log_info "Extracting Nextcloud..." + unzip -q "$tmpfile" -d "$rootfs/var/www/" || { + log_error "Failed to extract Nextcloud" + rm -f "$tmpfile" + return 1 + } + rm -f "$tmpfile" + + # Set ownership (will be fixed inside container) + chown -R 33:33 "$nc_dir" 2>/dev/null || true + + log_info "Nextcloud installed" + return 0 +} + +# Create startup script +create_startup_script() { + local rootfs="$LXC_ROOTFS" + + cat > "$rootfs/opt/start-nextcloud.sh" << 'STARTUP' +#!/bin/bash +set -e + +export PATH="/usr/sbin:/usr/bin:/sbin:/bin" + +# Start MariaDB +echo "Starting MariaDB..." +service mariadb start +sleep 2 + +# Start Redis +echo "Starting Redis..." +service redis-server start + +# Configure PHP-FPM pool +PHP_VERSION=$(ls /etc/php/ | head -1) +PHP_FPM_POOL="/etc/php/${PHP_VERSION}/fpm/pool.d/www.conf" + +# Increase PHP limits +sed -i "s/upload_max_filesize = .*/upload_max_filesize = ${NEXTCLOUD_UPLOAD_MAX:-512M}/" /etc/php/${PHP_VERSION}/fpm/php.ini +sed -i "s/post_max_size = .*/post_max_size = ${NEXTCLOUD_UPLOAD_MAX:-512M}/" /etc/php/${PHP_VERSION}/fpm/php.ini +sed -i "s/memory_limit = .*/memory_limit = 512M/" /etc/php/${PHP_VERSION}/fpm/php.ini +sed -i "s/max_execution_time = .*/max_execution_time = 3600/" /etc/php/${PHP_VERSION}/fpm/php.ini + +# Enable opcache +echo "opcache.enable=1" >> /etc/php/${PHP_VERSION}/fpm/php.ini +echo "opcache.memory_consumption=128" >> /etc/php/${PHP_VERSION}/fpm/php.ini +echo "opcache.interned_strings_buffer=16" >> /etc/php/${PHP_VERSION}/fpm/php.ini +echo "opcache.max_accelerated_files=10000" >> /etc/php/${PHP_VERSION}/fpm/php.ini + +# Start PHP-FPM +echo "Starting PHP-FPM..." +service php${PHP_VERSION}-fpm start + +# Configure Nginx +cat > /etc/nginx/sites-available/nextcloud << 'NGINX' +server { + listen 80; + server_name _; + + root /var/www/nextcloud; + index index.php index.html; + + client_max_body_size 512M; + fastcgi_buffers 64 4K; + + gzip on; + gzip_vary on; + gzip_comp_level 4; + gzip_min_length 256; + gzip_proxied expired no-cache no-store private no_last_modified no_etag auth; + gzip_types application/atom+xml text/javascript application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/wasm application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy; + + add_header Referrer-Policy "no-referrer" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Permitted-Cross-Domain-Policies "none" always; + add_header X-Robots-Tag "noindex, nofollow" always; + add_header X-XSS-Protection "1; mode=block" always; + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + + location ^~ /.well-known { + location = /.well-known/carddav { return 301 /remote.php/dav/; } + location = /.well-known/caldav { return 301 /remote.php/dav/; } + location /.well-known/acme-challenge { try_files $uri $uri/ =404; } + location /.well-known/pki-validation { try_files $uri $uri/ =404; } + return 301 /index.php$request_uri; + } + + location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) { return 404; } + location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; } + + location ~ \.php(?:$|/) { + rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php$request_uri; + + fastcgi_split_path_info ^(.+?\.php)(/.*)$; + set $path_info $fastcgi_path_info; + + try_files $fastcgi_script_name =404; + + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $path_info; + + fastcgi_param modHeadersAvailable true; + fastcgi_param front_controller_active true; + fastcgi_pass unix:/run/php/php-fpm.sock; + + fastcgi_intercept_errors on; + fastcgi_request_buffering off; + fastcgi_max_temp_file_size 0; + } + + location ~ \.(?:css|js|mjs|svg|gif|png|jpg|ico|wasm|tflite|map|ogg|flac)$ { + try_files $uri /index.php$request_uri; + add_header Cache-Control "public, max-age=15778463, immutable"; + access_log off; + } + + location ~ \.woff2?$ { + try_files $uri /index.php$request_uri; + expires 7d; + access_log off; + } + + location /remote { + return 301 /remote.php$request_uri; + } + + location / { + try_files $uri $uri/ /index.php$request_uri; + } +} +NGINX + +rm -f /etc/nginx/sites-enabled/default +ln -sf /etc/nginx/sites-available/nextcloud /etc/nginx/sites-enabled/nextcloud + +# Start Nginx (foreground for procd) +echo "Starting Nginx..." +exec nginx -g 'daemon off;' +STARTUP + chmod +x "$rootfs/opt/start-nextcloud.sh" +} + +# Setup database +setup_database() { + load_config + + # Generate password if not set + if [ -z "$db_password" ]; then + db_password=$(gen_password) + uci_set db.password "$db_password" + fi + + log_info "Setting up MariaDB database..." + + # Create setup script + cat > "$LXC_ROOTFS/tmp/setup-db.sh" << DBSCRIPT +#!/bin/bash +service mariadb start +sleep 2 + +mysql -u root << EOF +CREATE DATABASE IF NOT EXISTS ${db_name} CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +CREATE USER IF NOT EXISTS '${db_user}'@'localhost' IDENTIFIED BY '${db_password}'; +GRANT ALL PRIVILEGES ON ${db_name}.* TO '${db_user}'@'localhost'; +FLUSH PRIVILEGES; +EOF +DBSCRIPT + chmod +x "$LXC_ROOTFS/tmp/setup-db.sh" + + # Run via chroot + chroot "$LXC_ROOTFS" /tmp/setup-db.sh || { + log_error "Failed to setup database" + return 1 + } + + rm -f "$LXC_ROOTFS/tmp/setup-db.sh" + log_info "Database setup complete" +} + +# Configure Redis +setup_redis() { + load_config + local rootfs="$LXC_ROOTFS" + + log_info "Configuring Redis..." + + # Get memory in MB + local redis_mb="${redis_memory%M}" + redis_mb="${redis_mb%m}" + + # Configure Redis + cat > "$rootfs/etc/redis/redis.conf" << EOF +bind 127.0.0.1 +port 6379 +daemonize yes +pidfile /run/redis/redis-server.pid +loglevel notice +logfile /var/log/redis/redis-server.log +databases 16 +maxmemory ${redis_mb}mb +maxmemory-policy allkeys-lru +EOF + + log_info "Redis configured" +} + +# Create LXC config +lxc_create_config() { + load_config + + ensure_dir "$(dirname "$LXC_CONFIG")" + + # Convert memory limit to bytes + local mem_bytes + case "$memory_limit" in + *G|*g) mem_bytes=$((${memory_limit%[Gg]} * 1024 * 1024 * 1024)) ;; + *M|*m) mem_bytes=$((${memory_limit%[Mm]} * 1024 * 1024)) ;; + *K|*k) mem_bytes=$((${memory_limit%[Kk]} * 1024)) ;; + *) mem_bytes="$memory_limit" ;; + esac + + cat > "$LXC_CONFIG" << EOF +# Nextcloud Platform LXC Configuration +lxc.uts.name = $LXC_NAME +lxc.rootfs.path = dir:$LXC_ROOTFS +lxc.arch = $(uname -m) + +# Network: use host network +lxc.net.0.type = none + +# Mount points +lxc.mount.auto = proc:mixed sys:ro +lxc.mount.entry = $data_path/data var/www/nextcloud/data none bind,create=dir 0 0 +lxc.mount.entry = $data_path/config var/www/nextcloud/config none bind,create=dir 0 0 + +# Environment +lxc.environment = NEXTCLOUD_DOMAIN=$domain +lxc.environment = NEXTCLOUD_HTTP_PORT=$http_port +lxc.environment = NEXTCLOUD_UPLOAD_MAX=$upload_max +lxc.environment = NEXTCLOUD_TRUSTED_PROXIES=$trusted_proxies + +# Security +lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time sys_rawio + +# Init command +lxc.init.cmd = /opt/start-nextcloud.sh +EOF + + log_info "LXC config created" +} + +# Container control +lxc_running() { + lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING" +} + +lxc_exists() { + [ -f "$LXC_CONFIG" ] && [ -d "$LXC_ROOTFS" ] +} + +lxc_stop() { + if lxc_running; then + log_info "Stopping Nextcloud container..." + lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true + sleep 2 + fi +} + +lxc_run() { + load_config + lxc_stop + + if ! lxc_exists; then + log_error "Container not installed. Run: nextcloudctl install" + return 1 + fi + + # Regenerate config in case settings changed + lxc_create_config + + log_info "Starting Nextcloud container..." + exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG" +} + +# Execute command in container +lxc_exec() { + if ! lxc_running; then + log_error "Container not running" + return 1 + fi + lxc-attach -n "$LXC_NAME" -- "$@" +} + +# Run OCC command +lxc_occ() { + if ! lxc_running; then + log_error "Container not running" + return 1 + fi + lxc-attach -n "$LXC_NAME" -- su -s /bin/bash www-data -c "php /var/www/nextcloud/occ $*" +} + +# Commands +cmd_install() { + require_root + load_config + + log_info "Installing Nextcloud Platform..." + + lxc_check_prereqs || exit 1 + + # Generate admin password if not set + if [ -z "$admin_password" ]; then + admin_password=$(gen_password) + uci_set main.admin_password "$admin_password" + log_info "Generated admin password: $admin_password" + fi + + # Create container + if ! lxc_exists; then + lxc_create_rootfs || exit 1 + fi + + # Install packages + install_container_packages || exit 1 + + # Download Nextcloud + if [ ! -f "$LXC_ROOTFS/var/www/nextcloud/occ" ]; then + install_nextcloud || exit 1 + fi + + # Setup services + setup_database || exit 1 + setup_redis || exit 1 + + # Create startup script + create_startup_script + + # Create LXC config + lxc_create_config || exit 1 + + # Enable service + uci_set main.enabled '1' + /etc/init.d/nextcloud enable 2>/dev/null || true + + log_info "" + log_info "Installation complete!" + log_info "" + log_info "Start with: /etc/init.d/nextcloud start" + log_info "Web interface: http://:$http_port" + log_info "" + log_info "After first start, run Nextcloud setup:" + log_info " nextcloudctl occ maintenance:install \\" + log_info " --database mysql --database-name $db_name \\" + log_info " --database-user $db_user --database-pass '$db_password' \\" + log_info " --admin-user $admin_user --admin-pass '$admin_password'" + log_info "" +} + +cmd_uninstall() { + require_root + + log_info "Uninstalling Nextcloud Platform..." + + # Stop service + /etc/init.d/nextcloud stop 2>/dev/null || true + /etc/init.d/nextcloud disable 2>/dev/null || true + + lxc_stop + + # Remove container (keep data) + if [ -d "$LXC_PATH/$LXC_NAME" ]; then + rm -rf "$LXC_PATH/$LXC_NAME" + log_info "Container removed" + fi + + uci_set main.enabled '0' + + log_info "Nextcloud Platform uninstalled" + log_info "Data preserved in: $(uci_get main.data_path)" +} + +cmd_update() { + require_root + load_config + + if ! lxc_exists; then + log_error "Container not installed. Run: nextcloudctl install" + return 1 + fi + + log_info "Updating Nextcloud..." + + # Enable maintenance mode + if lxc_running; then + lxc_occ maintenance:mode --on + fi + + # Download new Nextcloud + install_nextcloud || exit 1 + + # Run upgrade + if lxc_running; then + lxc_occ upgrade + lxc_occ maintenance:mode --off + fi + + log_info "Update complete" +} + +cmd_status() { + load_config + + local enabled="$(uci_get main.enabled)" + local running="false" + local installed="false" + local version="" + local user_count=0 + local disk_used="0" + + if lxc_exists; then + installed="true" + fi + + if lxc_running; then + running="true" + + # Get Nextcloud version + version=$(lxc_occ -V 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo "") + + # Get user count + user_count=$(lxc_occ user:list --output=json 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | wc -l || echo 0) + fi + + # Get disk usage + if [ -d "$data_path/data" ]; then + disk_used=$(du -sh "$data_path/data" 2>/dev/null | awk '{print $1}' || echo "0") + fi + + # Get LAN IP + local lan_ip + lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1") + + cat << EOF +{ + "enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"), + "running": $running, + "installed": $installed, + "version": "$version", + "http_port": $http_port, + "data_path": "$data_path", + "memory_limit": "$memory_limit", + "domain": "$domain", + "user_count": $user_count, + "disk_used": "$disk_used", + "http_url": "http://${lan_ip}:${http_port}", + "ssl_enabled": $([ "$ssl_enabled" = "1" ] && echo "true" || echo "false"), + "ssl_domain": "$ssl_domain", + "redis_enabled": $([ "$redis_enabled" = "1" ] && echo "true" || echo "false"), + "container_name": "$LXC_NAME" +} +EOF +} + +cmd_logs() { + load_config + + if lxc_running; then + if [ "$1" = "-f" ]; then + lxc_exec tail -f /var/log/nginx/error.log /var/www/nextcloud/data/nextcloud.log 2>/dev/null + else + echo "=== Nginx Error Log ===" + lxc_exec tail -50 /var/log/nginx/error.log 2>/dev/null || echo "No logs" + echo "" + echo "=== Nextcloud Log ===" + lxc_exec tail -50 /var/www/nextcloud/data/nextcloud.log 2>/dev/null || echo "No logs" fi + else + echo "Container not running" + fi +} + +cmd_shell() { + require_root + + if ! lxc_running; then + log_error "Container not running" + exit 1 + fi + + lxc-attach -n "$LXC_NAME" -- /bin/bash +} + +cmd_occ() { + require_root + lxc_occ "$@" +} + +cmd_backup() { + require_root + load_config + + local name="${1:-$(date +%Y%m%d-%H%M%S)}" + local backup_dir="$BACKUP_PATH" + + ensure_dir "$backup_dir" + + log_info "Creating backup: $name" + + # Enable maintenance mode if running + local was_running=0 + if lxc_running; then + was_running=1 + lxc_occ maintenance:mode --on + fi + + # Dump database + log_info "Dumping database..." + if lxc_running; then + lxc_exec mysqldump -u "$db_user" -p"$db_password" "$db_name" > "$backup_dir/$name-db.sql" || { + log_error "Database dump failed" + [ $was_running -eq 1 ] && lxc_occ maintenance:mode --off + return 1 + } + fi + + # Backup data and config + log_info "Backing up data..." + tar -czf "$backup_dir/$name-data.tar.gz" \ + -C "$data_path" data config 2>/dev/null || { + log_error "Data backup failed" + [ $was_running -eq 1 ] && lxc_occ maintenance:mode --off + return 1 + } + + # Disable maintenance mode + if [ $was_running -eq 1 ]; then + lxc_occ maintenance:mode --off + fi + + local size=$(ls -lh "$backup_dir/$name-data.tar.gz" 2>/dev/null | awk '{print $5}') + log_info "Backup created: $backup_dir/$name-* ($size)" + + # Cleanup old backups + if [ "$backup_keep" -gt 0 ]; then + ls -t "$backup_dir"/*-db.sql 2>/dev/null | tail -n +$((backup_keep + 1)) | while read f; do + local base=$(basename "$f" -db.sql) + rm -f "$backup_dir/$base-db.sql" "$backup_dir/$base-data.tar.gz" + log_info "Removed old backup: $base" + done + fi + + echo "$name" +} + +cmd_restore() { + require_root + load_config + + local name="$1" + + if [ -z "$name" ]; then + log_error "Usage: nextcloudctl restore " + log_error "Available backups:" + cmd_list_backups + return 1 + fi + + local backup_dir="$BACKUP_PATH" + + if [ ! -f "$backup_dir/$name-db.sql" ]; then + log_error "Backup not found: $name" + return 1 + fi + + log_info "Restoring from backup: $name" + + # Enable maintenance mode + local was_running=0 + if lxc_running; then + was_running=1 + lxc_occ maintenance:mode --on + fi + + # Restore database + log_info "Restoring database..." + if lxc_running; then + lxc_exec mysql -u "$db_user" -p"$db_password" "$db_name" < "$backup_dir/$name-db.sql" || { + log_error "Database restore failed" + return 1 + } + fi + + # Restore data + log_info "Restoring data..." + tar -xzf "$backup_dir/$name-data.tar.gz" -C "$data_path" || { + log_error "Data restore failed" + return 1 + } + + # Fix permissions + if lxc_running; then + lxc_exec chown -R www-data:www-data /var/www/nextcloud + lxc_occ maintenance:mode --off + fi + + log_info "Restore complete" +} + +cmd_list_backups() { + load_config + + local backup_dir="$BACKUP_PATH" + + if [ ! -d "$backup_dir" ]; then + echo "No backups found" + return + fi + + echo "Available backups:" + ls -1 "$backup_dir"/*-db.sql 2>/dev/null | while read f; do + local name=$(basename "$f" -db.sql) + local data_file="$backup_dir/$name-data.tar.gz" + local size="N/A" + local date="N/A" + + if [ -f "$data_file" ]; then + size=$(ls -lh "$data_file" | awk '{print $5}') + fi + date=$(stat -c '%y' "$f" 2>/dev/null | cut -d. -f1) + + echo " $name ($size) $date" done } -check_prereqs() { - defaults - ensure_dir "$data_path" - [ -d /sys/fs/cgroup ] || { echo "[ERROR] /sys/fs/cgroup missing" >&2; return 1; } - ensure_packages dockerd docker containerd - /etc/init.d/dockerd enable >/dev/null 2>&1 - /etc/init.d/dockerd start >/dev/null 2>&1 +cmd_ssl_enable() { + require_root + load_config + + local domain="$1" + + if [ -z "$domain" ]; then + log_error "Usage: nextcloudctl ssl-enable " + return 1 + fi + + log_info "Enabling SSL for $domain via HAProxy..." + + # Check if HAProxy is available + if [ ! -x /usr/sbin/haproxyctl ]; then + log_error "HAProxy not installed. Install secubox-app-haproxy first." + return 1 + fi + + # Add vhost to HAProxy + local vhost_name="nextcloud_${domain//\./_}" + + uci add haproxy vhost + uci set haproxy.@vhost[-1].name="$vhost_name" + uci set haproxy.@vhost[-1].domain="$domain" + uci set haproxy.@vhost[-1].backend="nextcloud_backend" + uci set haproxy.@vhost[-1].ssl='1' + uci set haproxy.@vhost[-1].acme='1' + uci set haproxy.@vhost[-1].enabled='1' + + # Add backend + uci add haproxy backend + uci set haproxy.@backend[-1].name="nextcloud_backend" + uci set haproxy.@backend[-1].mode='http' + + # Add server + uci add haproxy server + uci set haproxy.@server[-1].backend="nextcloud_backend" + uci set haproxy.@server[-1].address="127.0.0.1" + uci set haproxy.@server[-1].port="$http_port" + + uci commit haproxy + + # Update Nextcloud config + uci_set ssl.enabled '1' + uci_set ssl.domain "$domain" + + # Reload HAProxy + /etc/init.d/haproxy reload 2>/dev/null || true + + # Add trusted domain in Nextcloud + if lxc_running; then + lxc_occ config:system:set trusted_domains 1 --value="$domain" + lxc_occ config:system:set overwriteprotocol --value="https" + fi + + log_info "SSL enabled for $domain" + log_info "Access Nextcloud at: https://$domain" } -pull_image() { defaults; docker pull "$image"; } +cmd_ssl_disable() { + require_root + load_config -stop_container() { docker stop "$CONTAINER" >/dev/null 2>&1 || true; docker rm "$CONTAINER" >/dev/null 2>&1 || true; } + log_info "Disabling SSL..." -cmd_install() { - require_root || { echo Root required >&2; exit 1; } - check_prereqs || exit 1 - ensure_dir "$data_path/html" - ensure_dir "$data_path/apps" - ensure_dir "$data_path/config" - ensure_dir "$data_path/data" - pull_image || exit 1 - uci set ${CONFIG}.main.enabled='1' - uci commit ${CONFIG} - /etc/init.d/nextcloud enable - echo "Nextcloud prerequisites installed." - echo "IMPORTANT: Set admin_password in /etc/config/nextcloud before starting" - echo "Start with: /etc/init.d/nextcloud start" - echo "Then configure trusted domains with: nextcloudctl occ config:system:set trusted_domains 1 --value=your-domain.com" -} + # Remove HAProxy config (simplified - just disable) + uci_set ssl.enabled '0' -cmd_check() { check_prereqs; echo "Prerequisite check completed."; } - -cmd_update() { - require_root || { echo Root required >&2; exit 1; } - pull_image || exit 1 - /etc/init.d/nextcloud restart -} - -cmd_status() { docker ps -a --filter "name=$CONTAINER"; } - -cmd_logs() { docker logs "$@" "$CONTAINER"; } - -cmd_occ() { - docker exec -u www-data "$CONTAINER" php occ "$@" + log_info "SSL disabled" } cmd_service_run() { - require_root || { echo Root required >&2; exit 1; } - check_prereqs || exit 1 - defaults - stop_container + require_root + load_config - local docker_args="--name $CONTAINER" - docker_args="$docker_args -p ${port}:80" - docker_args="$docker_args -v $data_path/html:/var/www/html" - docker_args="$docker_args -v $data_path/apps:/var/www/html/custom_apps" - docker_args="$docker_args -v $data_path/config:/var/www/html/config" - docker_args="$docker_args -v $data_path/data:/var/www/html/data" - docker_args="$docker_args -e TZ=$timezone" - - # Set admin credentials if provided - [ -n "$admin_user" ] && docker_args="$docker_args -e NEXTCLOUD_ADMIN_USER=$admin_user" - [ -n "$admin_password" ] && docker_args="$docker_args -e NEXTCLOUD_ADMIN_PASSWORD=$admin_password" - - # Set trusted domains - [ -n "$trusted_domains" ] && docker_args="$docker_args -e NEXTCLOUD_TRUSTED_DOMAINS=$trusted_domains" - - exec docker run --rm $docker_args "$image" + lxc_check_prereqs || exit 1 + lxc_run } -cmd_service_stop() { require_root || { echo Root required >&2; exit 1; }; stop_container; } +cmd_service_stop() { + require_root + lxc_stop +} +# Main case "${1:-}" in - install) shift; cmd_install "$@" ;; - check) shift; cmd_check "$@" ;; - update) shift; cmd_update "$@" ;; - status) shift; cmd_status "$@" ;; - logs) shift; cmd_logs "$@" ;; - occ) shift; cmd_occ "$@" ;; - service-run) shift; cmd_service_run "$@" ;; + install) shift; cmd_install "$@" ;; + uninstall) shift; cmd_uninstall "$@" ;; + update) shift; cmd_update "$@" ;; + start) /etc/init.d/nextcloud start ;; + stop) /etc/init.d/nextcloud stop ;; + restart) /etc/init.d/nextcloud restart ;; + status) shift; cmd_status "$@" ;; + logs) shift; cmd_logs "$@" ;; + shell) shift; cmd_shell "$@" ;; + occ) shift; cmd_occ "$@" ;; + backup) shift; cmd_backup "$@" ;; + restore) shift; cmd_restore "$@" ;; + list-backups) shift; cmd_list_backups "$@" ;; + ssl-enable) shift; cmd_ssl_enable "$@" ;; + ssl-disable) shift; cmd_ssl_disable "$@" ;; + service-run) shift; cmd_service_run "$@" ;; service-stop) shift; cmd_service_stop "$@" ;; - help|--help|-h|'') usage ;; - *) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;; + *) usage ;; esac