diff --git a/dist/backupper-2026.06.14.9.3-x86_64-1.txz.sha256 b/dist/backupper-2026.06.14.9.3-x86_64-1.txz.sha256 deleted file mode 100644 index 8d4c4de..0000000 --- a/dist/backupper-2026.06.14.9.3-x86_64-1.txz.sha256 +++ /dev/null @@ -1 +0,0 @@ -10324922ef4211fa691d4f0c659c3e4574c915d04826d57e1adfa86bc5de007d backupper-2026.06.14.9.3-x86_64-1.txz diff --git a/dist/backupper-2026.06.14.9.3-x86_64-1.txz b/dist/backupper-2026.06.14.9.4-x86_64-1.txz similarity index 55% rename from dist/backupper-2026.06.14.9.3-x86_64-1.txz rename to dist/backupper-2026.06.14.9.4-x86_64-1.txz index 9acfbd2..6a2514a 100644 Binary files a/dist/backupper-2026.06.14.9.3-x86_64-1.txz and b/dist/backupper-2026.06.14.9.4-x86_64-1.txz differ diff --git a/dist/backupper-2026.06.14.9.4-x86_64-1.txz.sha256 b/dist/backupper-2026.06.14.9.4-x86_64-1.txz.sha256 new file mode 100644 index 0000000..fa9ed71 --- /dev/null +++ b/dist/backupper-2026.06.14.9.4-x86_64-1.txz.sha256 @@ -0,0 +1 @@ +f1cd8773608d0e117ff2a71d5552dcc21b38259061d4399c8d4fa1f168c0373c backupper-2026.06.14.9.4-x86_64-1.txz diff --git a/dist/backupper.plg b/dist/backupper.plg index bab9434..00e3dc1 100644 --- a/dist/backupper.plg +++ b/dist/backupper.plg @@ -2,13 +2,17 @@ - + - + ]> +### 2026.06.14.9.4 +- Make the repository form type-aware for local, SMB, NFS, and SFTP destinations. +- Add a folder browser for local repository paths. + ### 2026.06.14.9.3 - Rename the visible product to URBM - Unraid Restic Backup Manager. - Show Backup in the Unraid navigation while preserving the existing plugin identifier and configuration paths. diff --git a/plugin/backupper.plg b/plugin/backupper.plg index d43f75d..da81f08 100644 --- a/plugin/backupper.plg +++ b/plugin/backupper.plg @@ -2,13 +2,17 @@ - + ]> +### 2026.06.14.9.4 +- Make the repository form type-aware for local, SMB, NFS, and SFTP destinations. +- Add a folder browser for local repository paths. + ### 2026.06.14.9.3 - Rename the visible product to URBM - Unraid Restic Backup Manager. - Show Backup in the Unraid navigation while preserving the existing plugin identifier and configuration paths. diff --git a/webgui/Backupper.page b/webgui/Backupper.page index 936b476..6941417 100644 --- a/webgui/Backupper.page +++ b/webgui/Backupper.page @@ -9,12 +9,12 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore" - +
- -

URBM 2026.06.14.9.3

Restic Backup Manager for Unraid

+ +

URBM 2026.06.14.9.4

Restic Backup Manager for Unraid

Connecting...
@@ -32,4 +32,4 @@ $pluginRoot = '/plugins/backupper';
- + diff --git a/webgui/assets/backupper.css b/webgui/assets/backupper.css index 046c39a..ea971d6 100644 --- a/webgui/assets/backupper.css +++ b/webgui/assets/backupper.css @@ -97,6 +97,11 @@ #backupper-app .bu-weekdays input:checked + span { background: var(--bu-accent) !important; border-color: var(--bu-accent) !important; color: #fff !important; } .bu-custom-cron { margin-top: 14px; } .bu-event-options { display: flex; flex-wrap: wrap; gap: 10px; } +.bu-form-grid { display: grid; gap: 15px; grid-template-columns: repeat(2, minmax(220px, 1fr)); } +.bu-managed-mount, .bu-external-mount { margin-top: 14px; } +.bu-managed-mount[hidden], .bu-external-mount[hidden] { display: none; } +#backupper-app .bu-inline-check { align-items: center; display: flex; gap: 9px; grid-template-columns: auto 1fr; } +#backupper-app .bu-inline-check input { margin: 0; } #backupper-app .bu-event-options label { align-items: center; display: flex; gap: 7px; grid-template-columns: auto auto; } #backupper-app .bu-event-options input[type="checkbox"] { margin: 0; min-height: 20px; width: 20px; } @@ -205,6 +210,7 @@ .bu-workload-options { grid-template-columns: 1fr; } .bu-retention-grid { grid-template-columns: 1fr; } .bu-schedule-grid { grid-template-columns: 1fr; } + .bu-form-grid { grid-template-columns: 1fr; } .bu-chart-grid-layout { grid-template-columns: 1fr; } .bu-header { align-items: flex-start; flex-direction: column; gap: 12px; } .bu-table { display: block; overflow-x: auto; } diff --git a/webgui/assets/backupper.js b/webgui/assets/backupper.js index 4e11373..3bdafe1 100644 --- a/webgui/assets/backupper.js +++ b/webgui/assets/backupper.js @@ -7,7 +7,8 @@ const toast = document.getElementById('bu-toast'); const tooltip = document.getElementById('bu-tooltip'); const sourceRoots = ['/mnt/user','/mnt/disks','/mnt/remotes','/boot'].map(path=>({name:path,path})); - const state = { config: null, runs: [], view: 'dashboard', snapshots: [], selectedSnapshot: null, snapshotRepo: null, files: [], browserPath: '/', restoreIncludes: [], backupSelections: [], workloadSelections: [], workloads: [], workloadAvailable: true, workloadError: '', sourceBrowserPath: '/', sourceDirectories: sourceRoots, sourceBrowserError: '' }; + const repositoryRoots = ['/mnt/user','/mnt/disks','/mnt/remotes'].map(path=>({name:path,path})); + const state = { config: null, runs: [], view: 'dashboard', snapshots: [], selectedSnapshot: null, snapshotRepo: null, files: [], browserPath: '/', restoreIncludes: [], backupSelections: [], workloadSelections: [], workloads: [], workloadAvailable: true, workloadError: '', sourceBrowserPath: '/', sourceDirectories: sourceRoots, sourceBrowserError: '', repositoryBrowserPath: '/', repositoryDirectories: repositoryRoots, repositoryBrowserError: '' }; const fieldHelp = { name: 'Frei wählbarer Anzeigename. Beispiel: Appdata täglich oder VM Home Assistant.', @@ -69,6 +70,8 @@ 'browser-up': 'Öffnet im Snapshot-Browser das übergeordnete Verzeichnis.', 'source-up': 'Öffnet im Ordner-Browser das übergeordnete Verzeichnis.', 'select-current-source': 'Wählt den aktuell geöffneten Ordner vollständig als Backup-Quelle aus.', + 'repository-up': 'Öffnet im lokalen Repository-Browser das übergeordnete Verzeichnis.', + 'select-repository-folder': 'Übernimmt den aktuell geöffneten Ordner als lokalen Repository-Pfad.', 'select-current-restore': 'Wählt den aktuell geöffneten Snapshot-Ordner vollständig für die Wiederherstellung aus.', }; @@ -109,6 +112,7 @@ 'browse': 'Öffnet den Dateibaum dieses Snapshots, um einzelne Dateien oder Ordner auszuwählen.', 'open-dir': 'Öffnet dieses Verzeichnis innerhalb des schreibgeschützten Snapshots.', 'open-source-dir': 'Öffnet diesen Ordner, um darin weitere Unterordner auszuwählen.', + 'open-repository-dir': 'Öffnet diesen lokalen Ordner, um ihn oder einen Unterordner als Repository-Ziel auszuwählen.', 'cancel-run': 'Bricht die laufende oder wartende Aufgabe ab. Cleanup und Neustart gestoppter Workloads werden trotzdem versucht.', }; return dynamic[name] || `${label} ausführen.`; @@ -374,15 +378,55 @@ } function repositoryForm(repo = {}) { + state.repositoryBrowserPath = '/'; state.repositoryDirectories = repositoryRoots; state.repositoryBrowserError = ''; content.innerHTML = `

${repo.id?'Edit':'New'} repository

- - - - - +
+
Restic encryption
${button('Save','submit-repo','primary')}${button('Cancel','cancel-form')}
`; enhanceTooltips(content); + renderRepositorySpecific(repo.type || 'local', repo); + } + + function repositoryLocationField(label, value, placeholder) { + return ``; + } + + function renderRepositorySpecific(type, repo = {}) { + const host = document.getElementById('bu-repository-specific'); + if (!host) return; + if (type === 'local') { + host.innerHTML = `${repositoryLocationField('Local repository folder',repo.location,'/mnt/user/backups/urbm')}
`; + renderRepositoryBrowser(); + } else if (type === 'smb' || type === 'nfs') { + const managed = repo.mount?.managed === true; + host.innerHTML = `
${type.toUpperCase()} destination
${repositoryLocationField('Existing mounted folder',repo.location,'/mnt/remotes/backup-nas')}
${type==='smb'?``:''}
`; + } else { + host.innerHTML = `
SFTP destination
${repositoryLocationField('SFTP repository location',repo.location,'backup@192.168.1.20:/srv/backups/urbm')}
`; + } + enhanceTooltips(host); + } + + function renderRepositoryBrowser() { + const host = document.getElementById('bu-repository-browser'); + if (!host) return; + const rows = state.repositoryDirectories.map(item=>`${esc(item.name)}${esc(item.path)}${button('Open',`open-repository-dir:${encodeURIComponent(item.path)}`)}`).join(''); + const error = state.repositoryBrowserError ? `
${esc(state.repositoryBrowserError)}
` : ''; + host.innerHTML = `

Local folder browser

${esc(state.repositoryBrowserPath)}
${state.repositoryBrowserPath!=='/'?button('Use this folder','select-repository-folder','primary'):''}${state.repositoryBrowserPath!=='/'?button('Up','repository-up'):''}
${error}
${rows||''}
NamePath
No subfolders found.
`; + enhanceTooltips(host); + } + + async function loadRepositoryDirectories(path) { + if (path === '/') { + state.repositoryBrowserPath='/'; state.repositoryDirectories=repositoryRoots; state.repositoryBrowserError=''; renderRepositoryBrowser(); return; + } + try { + state.repositoryDirectories=await api(`/v1/filesystem/directories?path=${encodeURIComponent(path)}`); + state.repositoryBrowserPath=path; state.repositoryBrowserError=''; + } catch (error) { + state.repositoryBrowserError=`Folder could not be loaded: ${error.message}`; + } + renderRepositoryBrowser(); } function renderSnapshots() { @@ -447,6 +491,9 @@ if (name === 'open-source-dir') { return loadSourceDirectories(decodeURIComponent(a)); } if (name === 'source-up') { const parent=state.sourceBrowserPath==='/'?'/':state.sourceBrowserPath.split('/').slice(0,-1).join('/')||'/'; return loadSourceDirectories(parent==='/mnt'?'/':parent); } if (name === 'select-current-source') { if(!state.backupSelections.includes(state.sourceBrowserPath)) state.backupSelections.push(state.sourceBrowserPath); return renderSourceBrowser(); } + if (name === 'open-repository-dir') { return loadRepositoryDirectories(decodeURIComponent(a)); } + if (name === 'repository-up') { const parent=state.repositoryBrowserPath==='/'?'/':state.repositoryBrowserPath.split('/').slice(0,-1).join('/')||'/'; return loadRepositoryDirectories(parent==='/mnt'?'/':parent); } + if (name === 'select-repository-folder') { const location=document.querySelector('#bu-repo-form [name="location"]'); if(location) location.value=state.repositoryBrowserPath; return; } if (name === 'open-dir') { state.browserPath=decodeURIComponent(a); state.files=await api(`/v1/repositories/${state.snapshotRepo}/snapshots/${state.selectedSnapshot}/files?path=${encodeURIComponent(state.browserPath)}`); return render(); } if (name === 'browser-up') { state.browserPath=state.browserPath.replace(/\/$/,'').split('/').slice(0,-1).join('/')||'/'; state.files=await api(`/v1/repositories/${state.snapshotRepo}/snapshots/${state.selectedSnapshot}/files?path=${encodeURIComponent(state.browserPath)}`); return render(); } if (name === 'select-current-restore') { if(!state.restoreIncludes.includes(state.browserPath)) state.restoreIncludes.push(state.browserPath); return render(); } @@ -471,10 +518,16 @@ state.config.jobs=state.config.jobs.filter(j=>j.id!==job.id).concat(job); await saveConfig(); render(); } if (kind === 'submit-repo') { - const f=new FormData(document.getElementById('bu-repo-form')); const repo={schemaVersion:1,id:f.get('id')||id('repo'),name:f.get('name').trim(),type:f.get('type'),location:f.get('location').trim(),passwordRef:f.get('passwordRef').trim(),credentialRef:f.get('credentialRef').trim(),mount:f.get('managed')==='on'?{managed:true,remote:f.get('remote').trim(),mountPoint:f.get('mountPoint').trim()}:null,options:f.get('type')==='sftp'?{knownHostsPath:f.get('knownHostsPath').trim()}:{}}; + const f=new FormData(document.getElementById('bu-repo-form')); const value=name=>String(f.get(name)||'').trim(); const type=value('type'); const managed=(type==='smb'||type==='nfs')&&f.get('managed')==='on'; + let location=managed?value('mountPoint'):value('location'); + if(!location) throw new Error(managed?'Mount point is required':'Repository location is required'); + if(managed&&!value('remote')) throw new Error('Remote share is required for a managed mount'); + let credentialRef=(type==='sftp'||(type==='smb'&&managed))?value('credentialRef'):''; + if(value('credential')&&!credentialRef) credentialRef=id(type==='sftp'?'sftp-key':'smb-credentials'); + const repo={schemaVersion:1,id:value('id')||id('repo'),name:value('name'),type,location,passwordRef:value('passwordRef'),credentialRef,mount:managed?{managed:true,remote:value('remote'),mountPoint:value('mountPoint')}:null,options:type==='sftp'?{knownHostsPath:value('knownHostsPath')}: {}}; if (volatileLocation(repo) && !confirm('WARNING: This destination is under /tmp or /run and will be lost after an Unraid reboot. Save it anyway?')) return; if(f.get('password')) await api(`/v1/secrets/${encodeURIComponent(repo.passwordRef)}`,{method:'PUT',body:JSON.stringify({type:'restic-password',value:f.get('password')})}); - if(f.get('credential')) { if(!repo.credentialRef) repo.credentialRef=id('mount-credential'); await api(`/v1/secrets/${encodeURIComponent(repo.credentialRef)}`,{method:'PUT',body:JSON.stringify({type:'mount-credential',value:f.get('credential')})}); } + if(f.get('credential')) await api(`/v1/secrets/${encodeURIComponent(repo.credentialRef)}`,{method:'PUT',body:JSON.stringify({type:type==='sftp'?'sftp-private-key':'smb-credential',value:f.get('credential')})}); state.config.repositories=state.config.repositories.filter(r=>r.id!==repo.id).concat(repo); await saveConfig(); state.config = await api('/v1/config'); @@ -491,6 +544,15 @@ document.querySelector('.bu-tabs').addEventListener('click', e => { const button=e.target.closest('[data-view]'); if(!button)return; state.view=button.dataset.view; document.querySelectorAll('.bu-tabs button').forEach(x=>x.classList.toggle('active',x===button)); render(); }); content.addEventListener('click', e => { const target=e.target.closest('[data-action]'); if(!target)return; e.preventDefault(); handle(target.dataset.action,target); }); content.addEventListener('change', e => { + if (e.target.name === 'type' && e.target.closest('#bu-repo-form')) { + renderRepositorySpecific(e.target.value); + } + if (e.target.name === 'managed' && e.target.closest('#bu-repo-form')) { + const fieldset=e.target.closest('.bu-fieldset'); + const managed=fieldset?.querySelector('.bu-managed-mount'); const external=fieldset?.querySelector('.bu-external-mount'); + if(managed) managed.hidden=!e.target.checked; + if(external) external.hidden=e.target.checked; + } if (e.target.name === 'scheduleMode') { const weekdays=e.target.closest('.bu-schedule')?.querySelector('.bu-weekdays'); if (weekdays) weekdays.hidden=e.target.value!=='selected';