diff --git a/applications/luci-app-uci-git-backup/Makefile b/applications/luci-app-uci-git-backup/Makefile new file mode 100644 index 000000000000..e737a6c25d7e --- /dev/null +++ b/applications/luci-app-uci-git-backup/Makefile @@ -0,0 +1,13 @@ +# This is free software, licensed under the GNU General Public License v2. + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=UCI Git Backup +LUCI_DEPENDS:=+luci-base +uci-git-backup + +PKG_LICENSE:=GPL-2.0-only +PKG_MAINTAINER:=Mathias Rangger + +include ../../luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/applications/luci-app-uci-git-backup/htdocs/luci-static/resources/view/uci-git-backup/restore.js b/applications/luci-app-uci-git-backup/htdocs/luci-static/resources/view/uci-git-backup/restore.js new file mode 100644 index 000000000000..c4484aa6abc0 --- /dev/null +++ b/applications/luci-app-uci-git-backup/htdocs/luci-static/resources/view/uci-git-backup/restore.js @@ -0,0 +1,120 @@ +'use strict'; +'require view'; +'require fs'; +'require ui'; + +function parseCommits(logText) { + return (logText || '').split(/\n/).filter(Boolean).map(function(line) { + const parts = line.split('\t'); + + if (parts.length < 3) + return null; + + return { + sha: parts[0], + date: parts[1], + msg: parts.slice(2).join('\t') + }; + }).filter(Boolean); +} + +function renderOutput(title, res, reloadAfterClose) { + const output = [res.stdout, res.stderr].filter(Boolean).join('\n').trim() || _('No output.'); + const success = (res.code === 0); + + ui.showModal(title, [ + E('p', { 'class': success ? 'spinning' : null }, [ + success ? _('Command completed successfully.') : _('Command failed with exit code %d.').format(res.code) + ]), + E('pre', { 'style': 'white-space: pre-wrap' }, [ output ]), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': function() { + ui.hideModal(); + + if (reloadAfterClose) + window.location.reload(); + } + }, [ _('Dismiss') ]) + ]) + ]); +} + +return view.extend({ + handleRestoreConfirm: function(sha) { + ui.showModal(_('Restoring backup...'), [ + E('p', { 'class': 'spinning' }, [ _('The selected commit is being restored now.') ]) + ]); + + return fs.exec('/usr/bin/uci-git-restore', [ sha ]).then(function(res) { + renderOutput(_('Restore Output'), res, res.code === 0); + }); + }, + + handleRestore: function(sha) { + ui.showModal(_('Restore this backup?'), [ + E('p', [ _('This will copy the selected backup into /etc/config/ and trigger a configuration reload.') ]), + E('p', [ _('Selected commit: %s').format(sha) ]), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': ui.hideModal }, [ _('Cancel') ]), + ' ', + E('button', { + 'class': 'btn cbi-button-action important', + 'click': ui.createHandlerFn(this, 'handleRestoreConfirm', sha) + }, [ _('Restore') ]) + ]) + ]); + }, + + load: function() { + return L.resolveDefault(fs.exec('/usr/bin/uci-git-list', []), { code: 1, stdout: '', stderr: '' }); + }, + + render: function(res) { + const commits = (res.code === 0) ? parseCommits(res.stdout) : []; + let body; + + if (res.code !== 0) { + body = E('div', { 'class': 'alert-message warning' }, [ + E('p', [ _('Unable to read backup history.') ]), + E('pre', { 'style': 'white-space: pre-wrap' }, [ (res.stderr || res.stdout || _('No error output.')).trim() ]) + ]); + } + else if (!commits.length) { + body = E('p', { 'class': 'cbi-section-descr' }, [ _('No backup commits found yet.') ]); + } + else { + const rows = commits.map(L.bind(function(commit) { + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, [ commit.date ]), + E('td', { 'class': 'td' }, [ commit.msg ]), + E('td', { 'class': 'td' }, [ E('code', [ commit.sha.substring(0, 8) ]) ]), + E('td', { 'class': 'td right' }, [ + E('button', { + 'class': 'btn cbi-button-action', + 'click': ui.createHandlerFn(this, 'handleRestore', commit.sha) + }, [ _('Restore') ]) + ]) + ]); + }, this)); + + body = E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, [ _('Date') ]), + E('th', { 'class': 'th' }, [ _('Commit') ]), + E('th', { 'class': 'th' }, [ _('SHA') ]), + E('th', { 'class': 'th right' }, [ _('Action') ]) + ]) + ].concat(rows)); + } + + return E([], [ + E('h2', [ _('Restore UCI Backup') ]), + E('div', { 'class': 'cbi-map-descr' }, [ + _('Select one of the last 30 backup commits and restore it. A new commit recording the restore will be created afterwards.') + ]), + body + ]); + } +}); diff --git a/applications/luci-app-uci-git-backup/htdocs/luci-static/resources/view/uci-git-backup/settings.js b/applications/luci-app-uci-git-backup/htdocs/luci-static/resources/view/uci-git-backup/settings.js new file mode 100644 index 000000000000..eba321203e5d --- /dev/null +++ b/applications/luci-app-uci-git-backup/htdocs/luci-static/resources/view/uci-git-backup/settings.js @@ -0,0 +1,245 @@ +'use strict'; +'require view'; +'require form'; +'require fs'; +'require ui'; +'require uci'; + +const CONFIG = 'uci_git_backup'; +const SECTION = 'config'; +const SSH_KEY_FILE = '/etc/uci-git-backup/id_rsa'; + +function getLocalFqdn() { + const host = (window.location && window.location.hostname) || 'openwrt'; + return host.trim() || 'openwrt'; +} + +function getDefaultAuthorName() { + return _('OpenWrt (%s)').format(getLocalFqdn()); +} + +function renderOutput(title, res) { + const output = [res.stdout, res.stderr].filter(Boolean).join('\n').trim() || _('No output.'); + const success = (res.code === 0); + + ui.showModal(title, [ + E('p', { 'class': success ? 'spinning' : null }, [ + success ? _('Command completed successfully.') : _('Command failed with exit code %d.').format(res.code) + ]), + E('pre', { 'style': 'white-space: pre-wrap' }, [output]), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': ui.hideModal }, [ _('Dismiss') ]) + ]) + ]); +} + +function readRecentBackupLog() { + return L.resolveDefault(fs.exec('/sbin/logread', [ '-e', 'uci-git-backup' ]), { code: 1, stdout: '', stderr: '' }) + .then(function(res) { + return (res.stdout || '').trim(); + }); +} + +return view.extend({ + getCurrentSettingsValues: function() { + if (!this.settingsSection) + return {}; + + return this.settingsSection.formvalue(SECTION); + }, + + saveCurrentSettings: function() { + if (!this.map) + return Promise.resolve(); + + return this.map.parse().then(L.bind(function() { + return this.save(null, true); + }, this.map)); + }, + + handleRunBackup: function() { + ui.showModal(_('Saving settings...'), [ + E('p', { 'class': 'spinning' }, [ _('Saving the current form values before running the backup.') ]) + ]); + + return this.saveCurrentSettings().then(function() { + ui.showModal(_('Running backup...'), [ + E('p', { 'class': 'spinning' }, [ _('The backup script is running now.') ]) + ]); + + return fs.exec('/usr/bin/uci-git-backup', []).then(function(res) { + return readRecentBackupLog().then(function(logText) { + if (![res.stdout, res.stderr].filter(Boolean).join('\n').trim() && logText) + res.stdout = logText; + + renderOutput(_('Backup Output'), res); + }); + }); + }).catch(function(err) { + renderOutput(_('Backup Output'), { + code: 1, + stdout: '', + stderr: err?.message || String(err) + }); + }); + }, + + handleTestConnection: function() { + const values = this.getCurrentSettingsValues(); + const args = [ + '--remote-url', values.remote_url || '', + '--branch', values.branch || 'main', + '--auth-type', values.auth_type || 'password', + '--username', values.username || '', + '--password', values.password || '' + ]; + + ui.showModal(_('Saving settings...'), [ + E('p', { 'class': 'spinning' }, [ _('Saving the current form values before testing the connection.') ]) + ]); + + return this.saveCurrentSettings().then(function() { + ui.showModal(_('Testing connection...'), [ + E('p', { 'class': 'spinning' }, [ _('Checking remote reachability and authentication with the current form values.') ]) + ]); + + return fs.exec('/usr/bin/uci-git-test', args).then(function(res) { + renderOutput(_('Connection Test Output'), res); + }); + }).catch(function(err) { + renderOutput(_('Connection Test Output'), { + code: 1, + stdout: '', + stderr: err?.message || String(err) + }); + }); + }, + + load: function() { + return Promise.all([ + uci.load(CONFIG), + L.resolveDefault(fs.read(SSH_KEY_FILE), ''), + readRecentBackupLog() + ]); + }, + + render: function(data) { + const sshKey = data[1] || ''; + const logText = data[2] || ''; + const defaultAuthorName = getDefaultAuthorName(); + let m, s, o; + + m = new form.Map(CONFIG, _('UCI Git Backup'), + _('Automatically commits and pushes watched UCI configuration to a remote Git repository whenever a watched config is committed.')); + this.map = m; + + s = m.section(form.NamedSection, SECTION, 'uci_git_backup', _('Settings')); + this.settingsSection = s; + s.anonymous = false; + s.addremove = false; + + o = s.option(form.Flag, 'enabled', _('Enable automatic backup')); + o.default = o.disabled; + o.rmempty = false; + + o = s.option(form.Value, 'remote_url', _('Remote Repository URL'), + _('HTTPS or SSH URL, e.g. https://github.com/user/repo.git or git@github.com:user/repo.git')); + o.placeholder = 'https://github.com/user/router-backup.git'; + o.rmempty = false; + + o = s.option(form.Value, 'branch', _('Branch'), + _('Remote branch to push to. It will be created if it does not exist yet.')); + o.placeholder = 'main'; + o.default = 'main'; + o.rmempty = false; + + o = s.option(form.ListValue, 'auth_type', _('Authentication')); + o.value('password', _('Username / Password or Token (HTTPS)')); + o.value('ssh', _('SSH Private Key')); + o.default = 'password'; + + o = s.option(form.Value, 'username', _('Username'), + _('Required for HTTPS auth. For personal access tokens, use your normal Git username here and paste the token below.')); + o.depends('auth_type', 'password'); + o.rmempty = true; + + o = s.option(form.Value, 'password', _('Password / Token'), + _('For hosted Git services, use a personal access token instead of your account password.')); + o.depends('auth_type', 'password'); + o.password = true; + o.rmempty = true; + + o = s.option(form.TextValue, 'ssh_private_key', _('SSH Private Key'), + _('Stored in /etc/uci-git-backup/id_rsa, not in UCI. Paste an unencrypted key. Leaving this field empty keeps the existing key file.')); + o.depends('auth_type', 'ssh'); + o.rows = 10; + o.wrap = 'off'; + o.rmempty = true; + o.load = function() { + return sshKey; + }; + o.write = function(section_id, value) { + value = (value || '').replace(/\r\n/g, '\n'); + + if (value.trim().length < 20) + return; + + if (!value.endsWith('\n')) + value += '\n'; + + return fs.write(SSH_KEY_FILE, value).then(function() { + return fs.exec('/bin/chmod', [ '0600', SSH_KEY_FILE ]); + }); + }; + o.remove = function() {}; + + o = s.option(form.Value, 'repo_path', _('Local Repository Path'), + _('Use /etc/... for persistent storage or /tmp/... for temporary RAM-only storage.')); + o.placeholder = '/etc/uci-git-backup/repo'; + o.default = '/etc/uci-git-backup/repo'; + + o = s.option(form.Value, 'author_name', _('Commit Author Name')); + o.placeholder = defaultAuthorName; + o.default = defaultAuthorName; + o.cfgvalue = function(section_id) { + const value = uci.get(CONFIG, section_id, 'author_name'); + return (!value || value === 'OpenWRT' || value === 'OpenWrt') ? defaultAuthorName : value; + }; + + o = s.option(form.Value, 'author_email', _('Commit Author Email')); + o.placeholder = 'openwrt@localhost'; + o.default = 'openwrt@localhost'; + + o = s.option(form.DynamicList, 'extra_triggers', _('Additional UCI Configs to Watch'), + _('Extra config names beyond the built-in set. Restart the service after saving to apply trigger changes.')); + o.placeholder = 'custom_package'; + o.rmempty = true; + + s = m.section(form.NamedSection, SECTION, 'uci_git_backup', _('Actions')); + s.anonymous = false; + s.addremove = false; + + o = s.option(form.Button, '_run_backup', _('Run Backup Now'), + _('Saves the current form values, then runs the backup script and shows its output.')); + o.inputstyle = 'action'; + o.inputtitle = _('Run Backup Now'); + o.onclick = ui.createHandlerFn(this, 'handleRunBackup'); + + o = s.option(form.Button, '_test_connection', _('Test Connection'), + _('Saves the current form values, then tests the remote settings without creating a commit or push.')); + o.inputstyle = 'action'; + o.inputtitle = _('Test Connection'); + o.onclick = ui.createHandlerFn(this, 'handleTestConnection'); + + s = m.section(form.NamedSection, SECTION, 'uci_git_backup', _('Recent Log')); + s.anonymous = false; + s.addremove = false; + + o = s.option(form.DummyValue, '_recent_log', _('Recent Log Output')); + o.renderWidget = function() { + return E('pre', { 'style': 'white-space: pre-wrap' }, [ logText || _('No recent log output.') ]); + }; + + return m.render(); + } +}); diff --git a/applications/luci-app-uci-git-backup/po/de/uci-git-backup.po b/applications/luci-app-uci-git-backup/po/de/uci-git-backup.po new file mode 100644 index 000000000000..c33abf888d88 --- /dev/null +++ b/applications/luci-app-uci-git-backup/po/de/uci-git-backup.po @@ -0,0 +1,193 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-app-uci-git-backup 1.2.1\n" +"PO-Revision-Date: 2026-04-12 12:20+0200\n" +"Last-Translator: OpenCode\n" +"Language-Team: German\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "No output." +msgstr "Keine Ausgabe." + +msgid "Command completed successfully." +msgstr "Befehl erfolgreich abgeschlossen." + +#, javascript-format +msgid "Command failed with exit code %d." +msgstr "Befehl mit Exit-Code %d fehlgeschlagen." + +msgid "Dismiss" +msgstr "Schließen" + +msgid "Running backup..." +msgstr "Backup wird ausgeführt..." + +msgid "The backup script is running now." +msgstr "Das Backup-Skript wird jetzt ausgeführt." + +msgid "Backup Output" +msgstr "Backup-Ausgabe" + +msgid "Testing connection..." +msgstr "Verbindung wird getestet..." + +msgid "Saving settings..." +msgstr "Einstellungen werden gespeichert..." + +msgid "Saving the current form values before running the backup." +msgstr "Die aktuellen Formularwerte werden vor dem Start des Backups gespeichert." + +msgid "Saving the current form values before testing the connection." +msgstr "Die aktuellen Formularwerte werden vor dem Verbindungstest gespeichert." + +msgid "Checking remote reachability and authentication with the current form values." +msgstr "Erreichbarkeit und Authentifizierung des Remotes werden mit den aktuellen Formularwerten geprüft." + +msgid "Connection Test Output" +msgstr "Ausgabe des Verbindungstests" + +msgid "UCI Git Backup" +msgstr "UCI Git Backup" + +msgid "Automatically commits and pushes watched UCI configuration to a remote Git repository whenever a watched config is committed." +msgstr "Überwachte UCI-Konfigurationen werden automatisch in ein entferntes Git-Repository committet und gepusht, sobald eine überwachte Konfiguration übernommen wird." + +msgid "Settings" +msgstr "Einstellungen" + +msgid "Enable automatic backup" +msgstr "Automatisches Backup aktivieren" + +msgid "Remote Repository URL" +msgstr "URL des entfernten Repositorys" + +msgid "HTTPS or SSH URL, e.g. https://github.com/user/repo.git or git@github.com:user/repo.git" +msgstr "HTTPS- oder SSH-URL, z. B. https://github.com/user/repo.git oder git@github.com:user/repo.git" + +msgid "Branch" +msgstr "Branch" + +msgid "Remote branch to push to. It will be created if it does not exist yet." +msgstr "Entfernter Branch, auf den gepusht wird. Er wird erstellt, falls er noch nicht existiert." + +msgid "Authentication" +msgstr "Authentifizierung" + +msgid "Username / Password or Token (HTTPS)" +msgstr "Benutzername / Passwort oder Token (HTTPS)" + +msgid "SSH Private Key" +msgstr "Privater SSH-Schlüssel" + +msgid "Username" +msgstr "Benutzername" + +msgid "Required for HTTPS auth. For personal access tokens, use your normal Git username here and paste the token below." +msgstr "Für HTTPS-Authentifizierung erforderlich. Verwenden Sie bei Personal Access Tokens hier Ihren normalen Git-Benutzernamen und fügen Sie das Token unten ein." + +msgid "Password / Token" +msgstr "Passwort / Token" + +msgid "For hosted Git services, use a personal access token instead of your account password." +msgstr "Verwenden Sie bei gehosteten Git-Diensten statt Ihres Kontopassworts ein Personal Access Token." + +msgid "Stored in /etc/uci-git-backup/id_rsa, not in UCI. Paste an unencrypted key. Leaving this field empty keeps the existing key file." +msgstr "Wird in /etc/uci-git-backup/id_rsa gespeichert, nicht in UCI. Fügen Sie einen unverschlüsselten Schlüssel ein. Wenn dieses Feld leer bleibt, wird die vorhandene Schlüsseldatei beibehalten." + +msgid "Local Repository Path" +msgstr "Lokaler Repository-Pfad" + +msgid "Use /etc/... for persistent storage or /tmp/... for temporary RAM-only storage." +msgstr "Verwenden Sie /etc/... für persistenten Speicher oder /tmp/... für temporäre reine RAM-Speicherung." + +msgid "Commit Author Name" +msgstr "Name des Commit-Autors" + +msgid "Commit Author Email" +msgstr "E-Mail des Commit-Autors" + +msgid "Additional UCI Configs to Watch" +msgstr "Zusätzliche zu überwachende UCI-Konfigurationen" + +msgid "Extra config names beyond the built-in set. Restart the service after saving to apply trigger changes." +msgstr "Zusätzliche Konfigurationsnamen außerhalb der eingebauten Liste. Starten Sie den Dienst nach dem Speichern neu, damit Trigger-Änderungen übernommen werden." + +msgid "Actions" +msgstr "Aktionen" + +msgid "Run Backup Now" +msgstr "Backup jetzt ausführen" + +msgid "Saves the current form values, then runs the backup script and shows its output." +msgstr "Speichert die aktuellen Formularwerte, führt dann das Backup-Skript aus und zeigt dessen Ausgabe an." + +msgid "Test Connection" +msgstr "Verbindung testen" + +msgid "Saves the current form values, then tests the remote settings without creating a commit or push." +msgstr "Speichert die aktuellen Formularwerte und testet dann die Remote-Einstellungen, ohne einen Commit oder Push auszuführen." + +msgid "Recent Log" +msgstr "Letztes Log" + +msgid "Recent Log Output" +msgstr "Letzte Log-Ausgabe" + +msgid "No recent log output." +msgstr "Keine aktuelle Log-Ausgabe." + +msgid "Restoring backup..." +msgstr "Backup wird wiederhergestellt..." + +msgid "The selected commit is being restored now." +msgstr "Der ausgewählte Commit wird jetzt wiederhergestellt." + +msgid "Restore Output" +msgstr "Ausgabe der Wiederherstellung" + +msgid "Restore this backup?" +msgstr "Dieses Backup wiederherstellen?" + +msgid "This will copy the selected backup into /etc/config/ and trigger a configuration reload." +msgstr "Dadurch wird das ausgewählte Backup nach /etc/config/ kopiert und ein Neuladen der Konfiguration ausgelöst." + +#, javascript-format +msgid "Selected commit: %s" +msgstr "Ausgewählter Commit: %s" + +msgid "Cancel" +msgstr "Abbrechen" + +msgid "Restore" +msgstr "Wiederherstellen" + +msgid "Unable to read backup history." +msgstr "Backup-Verlauf kann nicht gelesen werden." + +msgid "No error output." +msgstr "Keine Fehlerausgabe." + +msgid "No backup commits found yet." +msgstr "Noch keine Backup-Commits gefunden." + +msgid "Date" +msgstr "Datum" + +msgid "Commit" +msgstr "Commit" + +msgid "SHA" +msgstr "SHA" + +msgid "Action" +msgstr "Aktion" + +msgid "Restore UCI Backup" +msgstr "UCI-Backup wiederherstellen" + +msgid "Select one of the last 30 backup commits and restore it. A new commit recording the restore will be created afterwards." +msgstr "Wählen Sie einen der letzten 30 Backup-Commits aus und stellen Sie ihn wieder her. Anschließend wird ein neuer Commit erstellt, der die Wiederherstellung festhält." diff --git a/applications/luci-app-uci-git-backup/po/templates/uci-git-backup.pot b/applications/luci-app-uci-git-backup/po/templates/uci-git-backup.pot new file mode 100644 index 000000000000..027c01bcc1af --- /dev/null +++ b/applications/luci-app-uci-git-backup/po/templates/uci-git-backup.pot @@ -0,0 +1,184 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +msgid "No output." +msgstr "" + +msgid "Command completed successfully." +msgstr "" + +#, javascript-format +msgid "Command failed with exit code %d." +msgstr "" + +msgid "Dismiss" +msgstr "" + +msgid "Running backup..." +msgstr "" + +msgid "The backup script is running now." +msgstr "" + +msgid "Backup Output" +msgstr "" + +msgid "Testing connection..." +msgstr "" + +msgid "Saving settings..." +msgstr "" + +msgid "Saving the current form values before running the backup." +msgstr "" + +msgid "Saving the current form values before testing the connection." +msgstr "" + +msgid "Checking remote reachability and authentication with the current form values." +msgstr "" + +msgid "Connection Test Output" +msgstr "" + +msgid "UCI Git Backup" +msgstr "" + +msgid "Automatically commits and pushes watched UCI configuration to a remote Git repository whenever a watched config is committed." +msgstr "" + +msgid "Settings" +msgstr "" + +msgid "Enable automatic backup" +msgstr "" + +msgid "Remote Repository URL" +msgstr "" + +msgid "HTTPS or SSH URL, e.g. https://github.com/user/repo.git or git@github.com:user/repo.git" +msgstr "" + +msgid "Branch" +msgstr "" + +msgid "Remote branch to push to. It will be created if it does not exist yet." +msgstr "" + +msgid "Authentication" +msgstr "" + +msgid "Username / Password or Token (HTTPS)" +msgstr "" + +msgid "SSH Private Key" +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Required for HTTPS auth. For personal access tokens, use your normal Git username here and paste the token below." +msgstr "" + +msgid "Password / Token" +msgstr "" + +msgid "For hosted Git services, use a personal access token instead of your account password." +msgstr "" + +msgid "Stored in /etc/uci-git-backup/id_rsa, not in UCI. Paste an unencrypted key. Leaving this field empty keeps the existing key file." +msgstr "" + +msgid "Local Repository Path" +msgstr "" + +msgid "Use /etc/... for persistent storage or /tmp/... for temporary RAM-only storage." +msgstr "" + +msgid "Commit Author Name" +msgstr "" + +msgid "Commit Author Email" +msgstr "" + +msgid "Additional UCI Configs to Watch" +msgstr "" + +msgid "Extra config names beyond the built-in set. Restart the service after saving to apply trigger changes." +msgstr "" + +msgid "Actions" +msgstr "" + +msgid "Run Backup Now" +msgstr "" + +msgid "Saves the current form values, then runs the backup script and shows its output." +msgstr "" + +msgid "Test Connection" +msgstr "" + +msgid "Saves the current form values, then tests the remote settings without creating a commit or push." +msgstr "" + +msgid "Recent Log" +msgstr "" + +msgid "Recent Log Output" +msgstr "" + +msgid "No recent log output." +msgstr "" + +msgid "Restoring backup..." +msgstr "" + +msgid "The selected commit is being restored now." +msgstr "" + +msgid "Restore Output" +msgstr "" + +msgid "Restore this backup?" +msgstr "" + +msgid "This will copy the selected backup into /etc/config/ and trigger a configuration reload." +msgstr "" + +#, javascript-format +msgid "Selected commit: %s" +msgstr "" + +msgid "Cancel" +msgstr "" + +msgid "Restore" +msgstr "" + +msgid "Unable to read backup history." +msgstr "" + +msgid "No error output." +msgstr "" + +msgid "No backup commits found yet." +msgstr "" + +msgid "Date" +msgstr "" + +msgid "Commit" +msgstr "" + +msgid "SHA" +msgstr "" + +msgid "Action" +msgstr "" + +msgid "Restore UCI Backup" +msgstr "" + +msgid "Select one of the last 30 backup commits and restore it. A new commit recording the restore will be created afterwards." +msgstr "" diff --git a/applications/luci-app-uci-git-backup/root/usr/share/luci/menu.d/luci-app-uci-git-backup.json b/applications/luci-app-uci-git-backup/root/usr/share/luci/menu.d/luci-app-uci-git-backup.json new file mode 100644 index 000000000000..cf59961349f5 --- /dev/null +++ b/applications/luci-app-uci-git-backup/root/usr/share/luci/menu.d/luci-app-uci-git-backup.json @@ -0,0 +1,32 @@ +{ + "admin/services/uci-git-backup": { + "title": "UCI Git Backup", + "order": 60, + "action": { + "type": "alias", + "path": "admin/services/uci-git-backup/settings" + }, + "depends": { + "acl": ["luci-app-uci-git-backup"], + "uci": { "uci_git_backup": true } + } + }, + + "admin/services/uci-git-backup/settings": { + "title": "Settings", + "order": 10, + "action": { + "type": "view", + "path": "uci-git-backup/settings" + } + }, + + "admin/services/uci-git-backup/restore": { + "title": "Restore", + "order": 20, + "action": { + "type": "view", + "path": "uci-git-backup/restore" + } + } +} diff --git a/applications/luci-app-uci-git-backup/root/usr/share/rpcd/acl.d/luci-app-uci-git-backup.json b/applications/luci-app-uci-git-backup/root/usr/share/rpcd/acl.d/luci-app-uci-git-backup.json new file mode 100644 index 000000000000..04ed97b8db42 --- /dev/null +++ b/applications/luci-app-uci-git-backup/root/usr/share/rpcd/acl.d/luci-app-uci-git-backup.json @@ -0,0 +1,23 @@ +{ + "luci-app-uci-git-backup": { + "description": "Grant access to luci-app-uci-git-backup", + "read": { + "uci": ["uci_git_backup", "system", "dhcp"], + "file": { + "/etc/uci-git-backup/id_rsa": ["read"], + "/sbin/logread": ["exec"], + "/usr/bin/uci-git-backup": ["exec"], + "/usr/bin/uci-git-test": ["exec"], + "/usr/bin/uci-git-list": ["exec"], + "/usr/bin/uci-git-restore": ["exec"] + } + }, + "write": { + "uci": ["uci_git_backup"], + "file": { + "/bin/chmod": ["exec"], + "/etc/uci-git-backup/id_rsa": ["write"] + } + } + } +}