diff --git a/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/secubox-p2p/api.js b/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/secubox-p2p/api.js index 51353061..17cd2864 100644 --- a/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/secubox-p2p/api.js +++ b/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/secubox-p2p/api.js @@ -213,6 +213,75 @@ var callSyncWGMirror = rpc.declare({ expect: { success: false, synced_peers: 0 } }); +// Gitea Integration +var callGetGiteaConfig = rpc.declare({ + object: 'luci.secubox-p2p', + method: 'get_gitea_config', + expect: {} +}); + +var callSetGiteaConfig = rpc.declare({ + object: 'luci.secubox-p2p', + method: 'set_gitea_config', + params: ['config'], + expect: { success: false } +}); + +var callCreateGiteaRepo = rpc.declare({ + object: 'luci.secubox-p2p', + method: 'create_gitea_repo', + params: ['name', 'description', 'private', 'init_readme'], + expect: { success: false } +}); + +var callListGiteaRepos = rpc.declare({ + object: 'luci.secubox-p2p', + method: 'list_gitea_repos', + expect: { success: false, repos: [] } +}); + +var callGetGiteaCommits = rpc.declare({ + object: 'luci.secubox-p2p', + method: 'get_gitea_commits', + params: ['limit'], + expect: { success: false, commits: [] } +}); + +var callPushGiteaBackup = rpc.declare({ + object: 'luci.secubox-p2p', + method: 'push_gitea_backup', + params: ['message', 'components'], + expect: { success: false } +}); + +var callPullGiteaBackup = rpc.declare({ + object: 'luci.secubox-p2p', + method: 'pull_gitea_backup', + params: ['commit_sha'], + expect: { success: false } +}); + +// Local Backup +var callCreateLocalBackup = rpc.declare({ + object: 'luci.secubox-p2p', + method: 'create_local_backup', + params: ['name', 'components'], + expect: { success: false } +}); + +var callListLocalBackups = rpc.declare({ + object: 'luci.secubox-p2p', + method: 'list_local_backups', + expect: { success: false, backups: [] } +}); + +var callRestoreLocalBackup = rpc.declare({ + object: 'luci.secubox-p2p', + method: 'restore_local_backup', + params: ['backup_id'], + expect: { success: false } +}); + return baseclass.extend({ // Peers getPeers: function() { return callGetPeers(); }, @@ -269,5 +338,21 @@ return baseclass.extend({ // WireGuard Mirror getWGMirrorConfig: function() { return callGetWGMirrorConfig(); }, setWGMirrorConfig: function(config) { return callSetWGMirrorConfig(config); }, - syncWGMirror: function() { return callSyncWGMirror(); } + syncWGMirror: function() { return callSyncWGMirror(); }, + + // Gitea Integration + getGiteaConfig: function() { return callGetGiteaConfig(); }, + setGiteaConfig: function(config) { return callSetGiteaConfig(config); }, + createGiteaRepo: function(name, description, isPrivate, initReadme) { + return callCreateGiteaRepo(name, description, isPrivate, initReadme); + }, + listGiteaRepos: function() { return callListGiteaRepos(); }, + getGiteaCommits: function(limit) { return callGetGiteaCommits(limit || 20); }, + pushGiteaBackup: function(message, components) { return callPushGiteaBackup(message, components); }, + pullGiteaBackup: function(commitSha) { return callPullGiteaBackup(commitSha); }, + + // Local Backup + createLocalBackup: function(name, components) { return callCreateLocalBackup(name, components); }, + listLocalBackups: function() { return callListLocalBackups(); }, + restoreLocalBackup: function(backupId) { return callRestoreLocalBackup(backupId); } }); diff --git a/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/hub.js b/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/hub.js index 290c0cdb..cbbe791f 100644 --- a/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/hub.js +++ b/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/hub.js @@ -144,7 +144,9 @@ return view.extend({ P2PAPI.getWireGuardConfig().catch(function() { return {}; }), P2PAPI.getHAProxyConfig().catch(function() { return {}; }), P2PAPI.getRegistry().catch(function() { return {}; }), - P2PAPI.healthCheck().catch(function() { return {}; }) + P2PAPI.healthCheck().catch(function() { return {}; }), + P2PAPI.getGiteaConfig().catch(function() { return {}; }), + P2PAPI.listLocalBackups().catch(function() { return { backups: [] }; }) ]).then(function(results) { self.peers = results[0].peers || []; self.settings = results[1] || {}; @@ -156,11 +158,39 @@ return view.extend({ self.registry = results[7] || {}; self.health = results[8] || {}; + // Populate Gitea config from backend + var giteaCfg = results[9] || {}; + if (giteaCfg.server_url) self.giteaConfig.serverUrl = giteaCfg.server_url; + if (giteaCfg.repo_name) self.giteaConfig.repoName = giteaCfg.repo_name; + if (giteaCfg.repo_owner) self.giteaConfig.repoOwner = giteaCfg.repo_owner; + if (giteaCfg.enabled) self.giteaConfig.enabled = !!giteaCfg.enabled; + if (giteaCfg.has_token) self.giteaConfig.hasToken = giteaCfg.has_token; + + // Populate local backups + var backupList = results[10] || {}; + if (backupList.backups) self.meshBackupConfig.snapshots = backupList.backups; + // Populate hubRegistry from API if (self.registry.base_url) self.hubRegistry.baseUrl = self.registry.base_url; if (self.registry.cache_enabled !== undefined) self.hubRegistry.cacheEnabled = self.registry.cache_enabled; if (self.registry.cache_ttl) self.hubRegistry.cacheTTL = self.registry.cache_ttl; + // If Gitea is configured, fetch commits + if (self.giteaConfig.enabled && self.giteaConfig.serverUrl) { + P2PAPI.getGiteaCommits(20).then(function(result) { + if (result.success && result.commits) { + self.giteaConfig.commits = result.commits.map(function(c) { + return { + sha: c.sha, + message: c.commit ? c.commit.message : c.message, + date: c.commit ? new Date(c.commit.author.date).getTime() : Date.now() + }; + }); + self.giteaConfig.lastFetch = Date.now(); + } + }).catch(function() {}); + } + return {}; }); }, @@ -1428,19 +1458,34 @@ return view.extend({ createMeshBackup: function() { var self = this; - var snapshot = { - id: 'snap-' + Date.now(), - timestamp: Date.now(), - targets: this.meshBackupConfig.targets.slice(), - peers: this.peers.length, - services: this.services.length - }; - this.meshBackupConfig.snapshots.unshift(snapshot); - if (this.meshBackupConfig.snapshots.length > this.meshBackupConfig.maxSnapshots) { - this.meshBackupConfig.snapshots.pop(); - } - this.meshBackupConfig.lastBackup = Date.now(); - ui.addNotification(null, E('p', '📸 Mesh backup created: ' + snapshot.id), 'info'); + var backupName = 'backup-' + new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19); + + ui.addNotification(null, E('p', '📸 Creating backup...'), 'info'); + + P2PAPI.createLocalBackup(backupName, { + configs: true, + packages: true, + scripts: true + }).then(function(result) { + if (result.success) { + // Update local snapshots list + self.meshBackupConfig.snapshots.unshift({ + id: result.backup_id, + timestamp: Date.now(), + size: result.size, + path: result.path + }); + if (self.meshBackupConfig.snapshots.length > self.meshBackupConfig.maxSnapshots) { + self.meshBackupConfig.snapshots.pop(); + } + self.meshBackupConfig.lastBackup = Date.now(); + ui.addNotification(null, E('p', '✅ Backup created: ' + result.backup_id + ' (' + result.size + ')'), 'success'); + } else { + ui.addNotification(null, E('p', '❌ Backup failed: ' + (result.error || 'Unknown error')), 'error'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', '❌ Backup error: ' + err.message), 'error'); + }); }, showBackupHistoryModal: function() { @@ -1479,7 +1524,21 @@ return view.extend({ }, restoreBackup: function(snapId) { + var self = this; ui.addNotification(null, E('p', '♻️ Restoring backup ' + snapId + '...'), 'info'); + + P2PAPI.restoreLocalBackup(snapId).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', '✅ Restored ' + result.files_restored + ' files from ' + snapId), 'success'); + if (result.pre_restore_backup) { + ui.addNotification(null, E('p', '💾 Pre-restore backup saved: ' + result.pre_restore_backup), 'info'); + } + } else { + ui.addNotification(null, E('p', '❌ Restore failed: ' + (result.error || 'Unknown error')), 'error'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', '❌ Restore error: ' + err.message), 'error'); + }); }, deleteBackup: function(snapId) { @@ -1548,29 +1607,72 @@ return view.extend({ fetchGiteaCommits: function() { var self = this; - if (!this.giteaConfig.serverUrl) { + if (!this.giteaConfig.enabled) { ui.addNotification(null, E('p', 'Configure Gitea server first'), 'warning'); return; } ui.addNotification(null, E('p', '🔄 Fetching commits from Gitea...'), 'info'); - // Simulate fetch - setTimeout(function() { - self.giteaConfig.commits = [ - { sha: 'abc1234', message: 'feat(p2p): Add mesh backup', date: Date.now() - 3600000 }, - { sha: 'def5678', message: 'fix(dns): Bridge sync issue', date: Date.now() - 7200000 }, - { sha: 'ghi9012', message: 'chore: Update dependencies', date: Date.now() - 86400000 } - ]; - self.giteaConfig.lastFetch = Date.now(); - ui.addNotification(null, E('p', '✅ Fetched ' + self.giteaConfig.commits.length + ' commits'), 'success'); - }, 1000); + + P2PAPI.getGiteaCommits(20).then(function(result) { + if (result.success && result.commits) { + self.giteaConfig.commits = result.commits.map(function(c) { + return { + sha: c.sha, + message: c.commit ? c.commit.message : c.message, + date: c.commit ? new Date(c.commit.author.date).getTime() : Date.now() + }; + }); + self.giteaConfig.lastFetch = Date.now(); + ui.addNotification(null, E('p', '✅ Fetched ' + self.giteaConfig.commits.length + ' commits'), 'success'); + } else { + ui.addNotification(null, E('p', '⚠️ ' + (result.error || 'Failed to fetch commits')), 'warning'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', '❌ Error: ' + err.message), 'error'); + }); }, pushToGitea: function() { - if (!this.giteaConfig.serverUrl) { + var self = this; + if (!this.giteaConfig.enabled) { ui.addNotification(null, E('p', 'Configure Gitea server first'), 'warning'); return; } + + var commitMsg = 'SecuBox backup ' + new Date().toISOString().substring(0, 19).replace('T', ' '); ui.addNotification(null, E('p', '📤 Pushing config to Gitea...'), 'info'); + + P2PAPI.pushGiteaBackup(commitMsg, {}).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', '✅ Pushed ' + result.files_pushed + ' files to Gitea'), 'success'); + // Refresh commits + self.refreshGiteaCommits(); + } else { + ui.addNotification(null, E('p', '❌ Push failed: ' + (result.error || 'Unknown error')), 'error'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', '❌ Error: ' + err.message), 'error'); + }); + }, + + pullFromGitea: function(commitSha) { + var self = this; + if (!this.giteaConfig.enabled) { + ui.addNotification(null, E('p', 'Configure Gitea server first'), 'warning'); + return; + } + + ui.addNotification(null, E('p', '📥 Pulling from Gitea' + (commitSha ? ' (commit ' + commitSha.substring(0, 7) + ')' : '') + '...'), 'info'); + + P2PAPI.pullGiteaBackup(commitSha || '').then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', '✅ Restored ' + result.files_restored + ' files from Gitea'), 'success'); + } else { + ui.addNotification(null, E('p', '❌ Pull failed: ' + (result.error || 'Unknown error')), 'error'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', '❌ Error: ' + err.message), 'error'); + }); }, createGiteaRepo: function() { @@ -1624,47 +1726,61 @@ return view.extend({ E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { var serverUrl = document.getElementById('create-gitea-url').value; var repoName = document.getElementById('create-repo-name').value; + var repoDesc = document.getElementById('create-repo-desc').value; var token = document.getElementById('create-gitea-token').value; + var isPrivate = document.getElementById('create-private').checked; + var initReadme = document.getElementById('create-init').checked; var pushState = document.getElementById('create-push-state').checked; - if (!serverUrl || !repoName) { - ui.addNotification(null, E('p', 'Server URL and repo name required'), 'warning'); + if (!serverUrl || !repoName || !token) { + ui.addNotification(null, E('p', 'Server URL, repo name and access token required'), 'warning'); return; } - // Save config - self.giteaConfig.serverUrl = serverUrl; - self.giteaConfig.repoName = repoName; - self.giteaConfig.token = token; - self.giteaConfig.repoOwner = 'secubox'; - self.giteaConfig.enabled = true; - ui.hideModal(); ui.addNotification(null, E('p', '➕ Creating repository ' + repoName + '...'), 'info'); - // Simulate repo creation - setTimeout(function() { - // Add initial commit - self.giteaConfig.commits = [{ - sha: 'init' + Date.now().toString(16).substring(0, 4), - message: 'Initial commit: SecuBox P2P Hub config', - date: Date.now() - }]; - self.giteaConfig.lastFetch = Date.now(); + // First save the Gitea config to backend + P2PAPI.setGiteaConfig({ + server_url: serverUrl, + repo_name: repoName, + access_token: token, + enabled: 1 + }).then(function() { + // Now create the repository via backend + return P2PAPI.createGiteaRepo(repoName, repoDesc, isPrivate, initReadme); + }).then(function(result) { + if (result.success) { + // Update local state + self.giteaConfig.serverUrl = serverUrl; + self.giteaConfig.repoName = result.repo_name || repoName; + self.giteaConfig.repoOwner = result.owner || ''; + self.giteaConfig.enabled = true; + self.giteaConfig.lastFetch = Date.now(); - ui.addNotification(null, E('p', '✅ Repository created: ' + repoName), 'success'); + ui.addNotification(null, E('p', '✅ Repository created: ' + repoName), 'success'); - if (pushState) { - setTimeout(function() { - self.giteaConfig.commits.unshift({ - sha: 'state' + Date.now().toString(16).substring(0, 4), - message: 'feat: Add current mesh state and config', - date: Date.now() + // Push current state if requested + if (pushState) { + ui.addNotification(null, E('p', '📤 Pushing current state...'), 'info'); + P2PAPI.pushGiteaBackup('Initial SecuBox mesh state', {}).then(function(pushResult) { + if (pushResult.success) { + ui.addNotification(null, E('p', '📤 Current state pushed (' + pushResult.files_pushed + ' files)'), 'success'); + // Refresh commits + self.refreshGiteaCommits(); + } else { + ui.addNotification(null, E('p', 'Push failed: ' + (pushResult.error || 'Unknown error')), 'error'); + } }); - ui.addNotification(null, E('p', '📤 Current state pushed to ' + repoName), 'success'); - }, 1000); + } else { + self.refreshGiteaCommits(); + } + } else { + ui.addNotification(null, E('p', 'Failed to create repo: ' + (result.error || 'Unknown error')), 'error'); } - }, 1500); + }).catch(function(err) { + ui.addNotification(null, E('p', 'Error: ' + err.message), 'error'); + }); } }, '➕ Create Repository') ]) ]); @@ -1707,28 +1823,90 @@ return view.extend({ E('div', { 'class': 'modal-actions' }, [ E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), E('button', { 'class': 'cbi-button', 'click': function() { - self.giteaConfig.serverUrl = document.getElementById('gitea-url').value; - self.giteaConfig.repoOwner = document.getElementById('gitea-owner').value; - self.giteaConfig.repoName = document.getElementById('gitea-repo').value; - self.giteaConfig.branch = document.getElementById('gitea-branch').value || 'main'; - self.giteaConfig.token = document.getElementById('gitea-token').value; - ui.hideModal(); - ui.addNotification(null, E('p', 'Gitea configuration saved'), 'info'); - if (self.giteaConfig.serverUrl) self.fetchGiteaCommits(); + var serverUrl = document.getElementById('gitea-url').value; + var repoOwner = document.getElementById('gitea-owner').value; + var repoName = document.getElementById('gitea-repo').value; + var token = document.getElementById('gitea-token').value; + + // Test connection via backend + ui.addNotification(null, E('p', '🔄 Testing connection...'), 'info'); + P2PAPI.setGiteaConfig({ + server_url: serverUrl, + repo_owner: repoOwner, + repo_name: repoName, + access_token: token + }).then(function() { + return P2PAPI.getGiteaCommits(5); + }).then(function(result) { + if (result.success) { + self.giteaConfig.serverUrl = serverUrl; + self.giteaConfig.repoOwner = repoOwner; + self.giteaConfig.repoName = repoName; + self.giteaConfig.token = token; + self.giteaConfig.enabled = true; + ui.hideModal(); + ui.addNotification(null, E('p', '✅ Connection successful! ' + result.commits.length + ' commits found'), 'success'); + self.refreshGiteaCommits(); + } else { + ui.addNotification(null, E('p', '⚠️ ' + (result.error || 'Connection failed')), 'warning'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', '❌ Error: ' + err.message), 'error'); + }); } }, 'Test Connection'), E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { - self.giteaConfig.serverUrl = document.getElementById('gitea-url').value; - self.giteaConfig.repoOwner = document.getElementById('gitea-owner').value; - self.giteaConfig.repoName = document.getElementById('gitea-repo').value; - self.giteaConfig.branch = document.getElementById('gitea-branch').value || 'main'; - self.giteaConfig.token = document.getElementById('gitea-token').value; - ui.hideModal(); - ui.addNotification(null, E('p', 'Gitea configuration saved'), 'info'); + var serverUrl = document.getElementById('gitea-url').value; + var repoOwner = document.getElementById('gitea-owner').value; + var repoName = document.getElementById('gitea-repo').value; + var branch = document.getElementById('gitea-branch').value || 'main'; + var token = document.getElementById('gitea-token').value; + + // Save config via backend + P2PAPI.setGiteaConfig({ + server_url: serverUrl, + repo_owner: repoOwner, + repo_name: repoName, + access_token: token, + enabled: 1 + }).then(function(result) { + if (result.success) { + self.giteaConfig.serverUrl = serverUrl; + self.giteaConfig.repoOwner = repoOwner; + self.giteaConfig.repoName = repoName; + self.giteaConfig.branch = branch; + self.giteaConfig.token = token; + self.giteaConfig.enabled = true; + ui.hideModal(); + ui.addNotification(null, E('p', '✅ Gitea configuration saved'), 'success'); + } else { + ui.addNotification(null, E('p', '❌ Failed to save config'), 'error'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', '❌ Error: ' + err.message), 'error'); + }); } }, 'Save') ]) ]); }, + refreshGiteaCommits: function() { + var self = this; + P2PAPI.getGiteaCommits(20).then(function(result) { + if (result.success && result.commits) { + self.giteaConfig.commits = result.commits.map(function(c) { + return { + sha: c.sha, + message: c.commit ? c.commit.message : c.message, + date: c.commit ? new Date(c.commit.author.date).getTime() : Date.now() + }; + }); + self.giteaConfig.lastFetch = Date.now(); + } + }).catch(function() { + // Silent fail for background refresh + }); + }, + // ==================== Component Sources Actions ==================== toggleComponentSource: function(key, enabled) { this.componentSources[key].enabled = enabled; diff --git a/package/secubox/secubox-p2p/root/etc/config/secubox-p2p b/package/secubox/secubox-p2p/root/etc/config/secubox-p2p index 99d81a7a..2ca63bd7 100644 --- a/package/secubox/secubox-p2p/root/etc/config/secubox-p2p +++ b/package/secubox/secubox-p2p/root/etc/config/secubox-p2p @@ -34,3 +34,22 @@ config maas 'maas' option enabled '0' option auto_register '1' option sync_interval '60' + +config gitea 'gitea' + option enabled '0' + option server_url '' + option repo_name 'secubox-backup' + option repo_owner '' + option access_token '' + option auto_backup '0' + option backup_interval '3600' + option backup_on_change '1' + option include_configs '1' + option include_packages '1' + option include_scripts '1' + +config backup 'backup' + option enabled '1' + option backup_dir '/etc/secubox/backups' + option max_backups '10' + option auto_cleanup '1' diff --git a/package/secubox/secubox-p2p/root/usr/libexec/rpcd/luci.secubox-p2p b/package/secubox/secubox-p2p/root/usr/libexec/rpcd/luci.secubox-p2p index 37b136eb..05a785fb 100644 --- a/package/secubox/secubox-p2p/root/usr/libexec/rpcd/luci.secubox-p2p +++ b/package/secubox/secubox-p2p/root/usr/libexec/rpcd/luci.secubox-p2p @@ -27,7 +27,17 @@ case "$1" in "set_haproxy_config": { "config": "object" }, "get_registry": {}, "register_url": { "short_url": "string", "target_url": "string" }, - "health_check": {} + "health_check": {}, + "get_gitea_config": {}, + "set_gitea_config": { "config": "object" }, + "create_gitea_repo": { "name": "string", "description": "string", "private": true, "init_readme": true }, + "list_gitea_repos": {}, + "get_gitea_commits": { "limit": 20 }, + "push_gitea_backup": { "message": "string", "components": "object" }, + "pull_gitea_backup": { "commit_sha": "string" }, + "create_local_backup": { "name": "string", "components": "object" }, + "list_local_backups": {}, + "restore_local_backup": { "backup_id": "string" } } EOF ;; @@ -219,6 +229,389 @@ EOF EOF ;; + get_gitea_config) + cat </dev/null) + repo_name=$(echo "$input" | jsonfilter -e '@.config.repo_name' 2>/dev/null) + repo_owner=$(echo "$input" | jsonfilter -e '@.config.repo_owner' 2>/dev/null) + access_token=$(echo "$input" | jsonfilter -e '@.config.access_token' 2>/dev/null) + enabled=$(echo "$input" | jsonfilter -e '@.config.enabled' 2>/dev/null) + auto_backup=$(echo "$input" | jsonfilter -e '@.config.auto_backup' 2>/dev/null) + backup_interval=$(echo "$input" | jsonfilter -e '@.config.backup_interval' 2>/dev/null) + + [ -n "$server_url" ] && uci set secubox-p2p.gitea.server_url="$server_url" + [ -n "$repo_name" ] && uci set secubox-p2p.gitea.repo_name="$repo_name" + [ -n "$repo_owner" ] && uci set secubox-p2p.gitea.repo_owner="$repo_owner" + [ -n "$access_token" ] && uci set secubox-p2p.gitea.access_token="$access_token" + [ -n "$enabled" ] && uci set secubox-p2p.gitea.enabled="$enabled" + [ -n "$auto_backup" ] && uci set secubox-p2p.gitea.auto_backup="$auto_backup" + [ -n "$backup_interval" ] && uci set secubox-p2p.gitea.backup_interval="$backup_interval" + uci commit secubox-p2p + + echo '{"success":true}' + ;; + + create_gitea_repo) + read input + repo_name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) + description=$(echo "$input" | jsonfilter -e '@.description' 2>/dev/null) + is_private=$(echo "$input" | jsonfilter -e '@.private' 2>/dev/null) + init_readme=$(echo "$input" | jsonfilter -e '@.init_readme' 2>/dev/null) + + server_url=$(uci -q get secubox-p2p.gitea.server_url) + access_token=$(uci -q get secubox-p2p.gitea.access_token) + + if [ -z "$server_url" ] || [ -z "$access_token" ]; then + echo '{"success":false,"error":"Gitea server URL and access token required"}' + exit 0 + fi + + if [ -z "$repo_name" ]; then + echo '{"success":false,"error":"Repository name required"}' + exit 0 + fi + + # Create repo via Gitea API + api_url="${server_url}/api/v1/user/repos" + [ "$is_private" = "true" ] && private_val="true" || private_val="false" + [ "$init_readme" = "true" ] && readme_val="true" || readme_val="false" + + response=$(curl -s -X POST "$api_url" \ + -H "Authorization: token $access_token" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"$repo_name\",\"description\":\"$description\",\"private\":$private_val,\"auto_init\":$readme_val}" \ + 2>/dev/null) + + if echo "$response" | jsonfilter -e '@.id' >/dev/null 2>&1; then + clone_url=$(echo "$response" | jsonfilter -e '@.clone_url' 2>/dev/null) + html_url=$(echo "$response" | jsonfilter -e '@.html_url' 2>/dev/null) + owner=$(echo "$response" | jsonfilter -e '@.owner.login' 2>/dev/null) + + # Save repo config + uci set secubox-p2p.gitea.repo_name="$repo_name" + uci set secubox-p2p.gitea.repo_owner="$owner" + uci set secubox-p2p.gitea.enabled=1 + uci commit secubox-p2p + + cat </dev/null || echo "Failed to create repository") + echo "{\"success\":false,\"error\":\"$error_msg\"}" + fi + ;; + + list_gitea_repos) + server_url=$(uci -q get secubox-p2p.gitea.server_url) + access_token=$(uci -q get secubox-p2p.gitea.access_token) + + if [ -z "$server_url" ] || [ -z "$access_token" ]; then + echo '{"success":false,"repos":[],"error":"Gitea not configured"}' + exit 0 + fi + + response=$(curl -s "${server_url}/api/v1/user/repos" \ + -H "Authorization: token $access_token" \ + 2>/dev/null) + + if [ -n "$response" ]; then + echo "{\"success\":true,\"repos\":$response}" + else + echo '{"success":false,"repos":[],"error":"Failed to fetch repositories"}' + fi + ;; + + get_gitea_commits) + read input + limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null || echo "20") + + server_url=$(uci -q get secubox-p2p.gitea.server_url) + access_token=$(uci -q get secubox-p2p.gitea.access_token) + repo_owner=$(uci -q get secubox-p2p.gitea.repo_owner) + repo_name=$(uci -q get secubox-p2p.gitea.repo_name) + + if [ -z "$server_url" ] || [ -z "$access_token" ] || [ -z "$repo_owner" ] || [ -z "$repo_name" ]; then + echo '{"success":false,"commits":[],"error":"Gitea repository not configured"}' + exit 0 + fi + + response=$(curl -s "${server_url}/api/v1/repos/${repo_owner}/${repo_name}/commits?limit=${limit}" \ + -H "Authorization: token $access_token" \ + 2>/dev/null) + + if [ -n "$response" ] && echo "$response" | jsonfilter -e '@[0].sha' >/dev/null 2>&1; then + echo "{\"success\":true,\"commits\":$response}" + else + echo '{"success":false,"commits":[],"error":"Failed to fetch commits or repository empty"}' + fi + ;; + + push_gitea_backup) + read input + message=$(echo "$input" | jsonfilter -e '@.message' 2>/dev/null || echo "SecuBox backup $(date +%Y%m%d-%H%M%S)") + components=$(echo "$input" | jsonfilter -e '@.components' 2>/dev/null) + + server_url=$(uci -q get secubox-p2p.gitea.server_url) + access_token=$(uci -q get secubox-p2p.gitea.access_token) + repo_owner=$(uci -q get secubox-p2p.gitea.repo_owner) + repo_name=$(uci -q get secubox-p2p.gitea.repo_name) + + if [ -z "$server_url" ] || [ -z "$access_token" ] || [ -z "$repo_owner" ] || [ -z "$repo_name" ]; then + echo '{"success":false,"error":"Gitea repository not configured"}' + exit 0 + fi + + # Create backup directory + backup_dir="/tmp/secubox-gitea-backup-$$" + mkdir -p "$backup_dir" + + # Collect configs + if [ "$(uci -q get secubox-p2p.gitea.include_configs)" = "1" ]; then + mkdir -p "$backup_dir/configs" + cp -r /etc/config/secubox* "$backup_dir/configs/" 2>/dev/null + cp -r /etc/config/network "$backup_dir/configs/" 2>/dev/null + cp -r /etc/config/firewall "$backup_dir/configs/" 2>/dev/null + cp -r /etc/config/wireless "$backup_dir/configs/" 2>/dev/null + fi + + # Collect package list + if [ "$(uci -q get secubox-p2p.gitea.include_packages)" = "1" ]; then + mkdir -p "$backup_dir/packages" + opkg list-installed > "$backup_dir/packages/installed.txt" 2>/dev/null + fi + + # Collect scripts + if [ "$(uci -q get secubox-p2p.gitea.include_scripts)" = "1" ]; then + mkdir -p "$backup_dir/scripts" + cp -r /etc/secubox/scripts/* "$backup_dir/scripts/" 2>/dev/null + fi + + # Create manifest + cat > "$backup_dir/manifest.json" </dev/null || echo "unknown")", + "message": "$message" +} +MANIFEST + + # Push each file via Gitea API + pushed_files=0 + api_base="${server_url}/api/v1/repos/${repo_owner}/${repo_name}/contents" + + for file in $(find "$backup_dir" -type f); do + rel_path="${file#$backup_dir/}" + content=$(base64 "$file" | tr -d '\n') + + # Check if file exists (to update vs create) + existing=$(curl -s "${api_base}/${rel_path}" \ + -H "Authorization: token $access_token" 2>/dev/null) + sha=$(echo "$existing" | jsonfilter -e '@.sha' 2>/dev/null) + + if [ -n "$sha" ]; then + # Update existing file + curl -s -X PUT "${api_base}/${rel_path}" \ + -H "Authorization: token $access_token" \ + -H "Content-Type: application/json" \ + -d "{\"message\":\"$message\",\"content\":\"$content\",\"sha\":\"$sha\"}" \ + >/dev/null 2>&1 + else + # Create new file + curl -s -X POST "${api_base}/${rel_path}" \ + -H "Authorization: token $access_token" \ + -H "Content-Type: application/json" \ + -d "{\"message\":\"$message\",\"content\":\"$content\"}" \ + >/dev/null 2>&1 + fi + pushed_files=$((pushed_files + 1)) + done + + # Cleanup + rm -rf "$backup_dir" + + echo "{\"success\":true,\"files_pushed\":$pushed_files,\"message\":\"$message\"}" + ;; + + pull_gitea_backup) + read input + commit_sha=$(echo "$input" | jsonfilter -e '@.commit_sha' 2>/dev/null) + + server_url=$(uci -q get secubox-p2p.gitea.server_url) + access_token=$(uci -q get secubox-p2p.gitea.access_token) + repo_owner=$(uci -q get secubox-p2p.gitea.repo_owner) + repo_name=$(uci -q get secubox-p2p.gitea.repo_name) + + if [ -z "$server_url" ] || [ -z "$access_token" ] || [ -z "$repo_owner" ] || [ -z "$repo_name" ]; then + echo '{"success":false,"error":"Gitea repository not configured"}' + exit 0 + fi + + # Get file tree at commit + ref_param="" + [ -n "$commit_sha" ] && ref_param="?ref=$commit_sha" + + tree=$(curl -s "${server_url}/api/v1/repos/${repo_owner}/${repo_name}/contents${ref_param}" \ + -H "Authorization: token $access_token" 2>/dev/null) + + if [ -z "$tree" ]; then + echo '{"success":false,"error":"Failed to fetch repository contents"}' + exit 0 + fi + + # Create restore directory + restore_dir="/tmp/secubox-restore-$$" + mkdir -p "$restore_dir" + restored_files=0 + + # Download configs directory + configs=$(curl -s "${server_url}/api/v1/repos/${repo_owner}/${repo_name}/contents/configs${ref_param}" \ + -H "Authorization: token $access_token" 2>/dev/null) + + if echo "$configs" | jsonfilter -e '@[0].name' >/dev/null 2>&1; then + mkdir -p "$restore_dir/configs" + for file_info in $(echo "$configs" | jsonfilter -e '@[*].name'); do + file_content=$(curl -s "${server_url}/api/v1/repos/${repo_owner}/${repo_name}/contents/configs/${file_info}${ref_param}" \ + -H "Authorization: token $access_token" 2>/dev/null) + content_b64=$(echo "$file_content" | jsonfilter -e '@.content' 2>/dev/null) + if [ -n "$content_b64" ]; then + echo "$content_b64" | base64 -d > "$restore_dir/configs/$file_info" + restored_files=$((restored_files + 1)) + fi + done + + # Apply configs (with backup) + if [ -d "$restore_dir/configs" ]; then + cp -r /etc/config /etc/config.bak.$(date +%Y%m%d%H%M%S) 2>/dev/null + for cfg in "$restore_dir"/configs/secubox*; do + [ -f "$cfg" ] && cp "$cfg" /etc/config/ 2>/dev/null + done + fi + fi + + # Cleanup + rm -rf "$restore_dir" + + echo "{\"success\":true,\"files_restored\":$restored_files,\"commit\":\"${commit_sha:-HEAD}\"}" + ;; + + create_local_backup) + read input + backup_name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null || echo "backup-$(date +%Y%m%d-%H%M%S)") + + backup_base=$(uci -q get secubox-p2p.backup.backup_dir || echo "/etc/secubox/backups") + mkdir -p "$backup_base" + + backup_dir="$backup_base/$backup_name" + mkdir -p "$backup_dir" + + # Backup configs + mkdir -p "$backup_dir/configs" + cp -r /etc/config/secubox* "$backup_dir/configs/" 2>/dev/null + cp /etc/config/network "$backup_dir/configs/" 2>/dev/null + cp /etc/config/firewall "$backup_dir/configs/" 2>/dev/null + cp /etc/config/wireless "$backup_dir/configs/" 2>/dev/null + + # Backup package list + opkg list-installed > "$backup_dir/packages.txt" 2>/dev/null + + # Create manifest + cat > "$backup_dir/manifest.json" </dev/null || echo "unknown")" +} +MANIFEST + + # Cleanup old backups + max_backups=$(uci -q get secubox-p2p.backup.max_backups || echo 10) + if [ "$(uci -q get secubox-p2p.backup.auto_cleanup)" = "1" ]; then + ls -1dt "$backup_base"/*/ 2>/dev/null | tail -n +$((max_backups + 1)) | xargs rm -rf 2>/dev/null + fi + + backup_size=$(du -sh "$backup_dir" 2>/dev/null | cut -f1) + echo "{\"success\":true,\"backup_id\":\"$backup_name\",\"path\":\"$backup_dir\",\"size\":\"$backup_size\"}" + ;; + + list_local_backups) + backup_base=$(uci -q get secubox-p2p.backup.backup_dir || echo "/etc/secubox/backups") + mkdir -p "$backup_base" + + echo '{"success":true,"backups":[' + first=1 + for dir in "$backup_base"/*/; do + [ -d "$dir" ] || continue + backup_id=$(basename "$dir") + if [ -f "$dir/manifest.json" ]; then + timestamp=$(jsonfilter -i "$dir/manifest.json" -e '@.timestamp' 2>/dev/null || echo "") + hostname=$(jsonfilter -i "$dir/manifest.json" -e '@.hostname' 2>/dev/null || echo "") + else + timestamp="" + hostname="" + fi + size=$(du -sh "$dir" 2>/dev/null | cut -f1) + [ $first -eq 1 ] || echo "," + first=0 + echo "{\"id\":\"$backup_id\",\"timestamp\":\"$timestamp\",\"hostname\":\"$hostname\",\"size\":\"$size\"}" + done + echo ']}' + ;; + + restore_local_backup) + read input + backup_id=$(echo "$input" | jsonfilter -e '@.backup_id' 2>/dev/null) + + backup_base=$(uci -q get secubox-p2p.backup.backup_dir || echo "/etc/secubox/backups") + backup_dir="$backup_base/$backup_id" + + if [ ! -d "$backup_dir" ]; then + echo '{"success":false,"error":"Backup not found"}' + exit 0 + fi + + # Create pre-restore backup + pre_restore="$backup_base/pre-restore-$(date +%Y%m%d-%H%M%S)" + mkdir -p "$pre_restore/configs" + cp -r /etc/config/secubox* "$pre_restore/configs/" 2>/dev/null + + # Restore configs + restored=0 + if [ -d "$backup_dir/configs" ]; then + for cfg in "$backup_dir"/configs/*; do + [ -f "$cfg" ] && cp "$cfg" /etc/config/ 2>/dev/null && restored=$((restored + 1)) + done + fi + + echo "{\"success\":true,\"files_restored\":$restored,\"pre_restore_backup\":\"$pre_restore\"}" + ;; + *) echo '{"error":"Unknown method"}' ;;