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 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-16 07:49:17 +01:00
parent 59d0e89a8c
commit 09b40c3b88
12 changed files with 2082 additions and 408 deletions

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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
}
});

View File

@ -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 <<EOF
@ -49,40 +64,50 @@ get_status() {
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
"installed": $([ "$installed" = "1" ] && echo "true" || echo "false"),
"docker_available": $([ "$docker_available" = "1" ] && echo "true" || echo "false"),
"container_status": "$container_status",
"port": ${port:-80},
"version": "$version",
"http_port": ${http_port:-8080},
"data_path": "${data_path:-/srv/nextcloud}",
"admin_user": "${admin_user:-admin}",
"trusted_domains": "${trusted_domains:-cloud.local}",
"image": "${image:-nextcloud:latest}",
"domain": "${domain:-cloud.local}",
"user_count": $user_count,
"disk_used": "$disk_used",
"web_url": "http://${lan_ip}:${http_port:-8080}",
"web_accessible": $([ "$web_accessible" = "1" ] && echo "true" || echo "false"),
"web_url": "http://192.168.255.1:${port:-80}"
"ssl_enabled": $([ "$ssl_enabled" = "1" ] && echo "true" || echo "false"),
"ssl_domain": "${ssl_domain:-}",
"container_name": "$LXC_NAME"
}
EOF
}
# Get configuration
get_config() {
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 admin_password=$(uci_get admin_password)
local trusted_domains=$(uci_get trusted_domains)
local timezone=$(uci_get timezone)
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 admin_user=$(uci_get main.admin_user)
local memory_limit=$(uci_get main.memory_limit)
local upload_max=$(uci_get main.upload_max)
local redis_enabled=$(uci_get redis.enabled)
local ssl_enabled=$(uci_get ssl.enabled)
local ssl_domain=$(uci_get ssl.domain)
local backup_enabled=$(uci_get backup.enabled)
local backup_keep=$(uci_get backup.keep)
cat <<EOF
{
"enabled": "${enabled:-0}",
"port": "${port:-80}",
"http_port": "${http_port:-8080}",
"data_path": "${data_path:-/srv/nextcloud}",
"domain": "${domain:-cloud.local}",
"admin_user": "${admin_user:-admin}",
"admin_password": "${admin_password:-}",
"trusted_domains": "${trusted_domains:-cloud.local}",
"timezone": "${timezone:-UTC}",
"image": "${image:-nextcloud:latest}"
"memory_limit": "${memory_limit:-1G}",
"upload_max": "${upload_max:-512M}",
"redis_enabled": "${redis_enabled:-1}",
"ssl_enabled": "${ssl_enabled:-0}",
"ssl_domain": "${ssl_domain:-}",
"backup_enabled": "${backup_enabled:-1}",
"backup_keep": "${backup_keep:-7}"
}
EOF
}
@ -92,19 +117,19 @@ save_config() {
local input
read -r input
local port=$(echo "$input" | jsonfilter -e '@.port' 2>/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
;;
*)

View File

@ -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"

View File

@ -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"]
}

View File

@ -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

View File

@ -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'

View File

@ -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"
}