Remove automatic repository subdirectories
This commit is contained in:
+4
-4
@@ -9,12 +9,12 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore"
|
||||
<?php
|
||||
$pluginRoot = '/plugins/urbm';
|
||||
?>
|
||||
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=20260614r004">
|
||||
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=20260614r005">
|
||||
<div id="urbm-app" data-api="<?= $pluginRoot ?>/api.php">
|
||||
<header class="bu-header">
|
||||
<div class="bu-brand">
|
||||
<img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260614r004" alt="URBM-Logo">
|
||||
<div><h1>URBM <span class="bu-version">2026.06.14.r004</span></h1><p>Restic Backup Manager für Unraid</p></div>
|
||||
<img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260614r005" alt="URBM-Logo">
|
||||
<div><h1>URBM <span class="bu-version">2026.06.14.r005</span></h1><p>Restic Backup Manager für Unraid</p></div>
|
||||
</div>
|
||||
<div id="bu-health" class="bu-health" tabindex="0" data-tooltip="Zeigt, ob die URBM-Hintergrundkomponente erreichbar ist. Beispiel: 'Daemon online' bedeutet, dass Jobs gestartet werden können.">Verbindung wird hergestellt...</div>
|
||||
</header>
|
||||
@@ -32,4 +32,4 @@ $pluginRoot = '/plugins/urbm';
|
||||
<div id="bu-tooltip" role="tooltip" aria-hidden="true"></div>
|
||||
</div>
|
||||
<script>window.URBM_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script>
|
||||
<script src="<?= $pluginRoot ?>/assets/urbm-2026.06.14.r004.js"></script>
|
||||
<script src="<?= $pluginRoot ?>/assets/urbm-2026.06.14.r005.js"></script>
|
||||
|
||||
+6
-33
@@ -27,7 +27,7 @@
|
||||
keepWeekly: 'Zusätzliche wöchentliche Stände außerhalb der Altersfrist. 0 deaktiviert diese Zusatzregel.',
|
||||
keepMonthly: 'Zusätzliche monatliche Stände außerhalb der Altersfrist. 0 deaktiviert diese Zusatzregel.',
|
||||
enabled: 'Aktiviert automatische geplante Läufe. Deaktivierte Jobs können weiterhin gespeichert, aber nicht automatisch ausgeführt werden.',
|
||||
location: 'Bei neuen Repositorys ist dies der Basisordner. URBM ergänzt automatisch einen sicheren Unterordner aus dem Repository-Namen. Beispiel: /mnt/user/Backups und Fotos Zuhause ergeben /mnt/user/Backups/Fotos-Zuhause.',
|
||||
location: 'Restic-Ziel. Lokal: /mnt/user/backups/job1. SFTP: user@server:/backup/job1. Bei SMB/NFS mit Mount wird hier der Mount-Pfad verwendet.',
|
||||
password: 'Passwort zur Restic-Verschlüsselung. Beispiel: eine lange zufällige Passphrase. Leer lassen, wenn ein vorhandenes Passwort unverändert bleiben soll.',
|
||||
managed: 'Wenn aktiv, mountet URBM SMB/NFS vor dem Job selbst. Wenn aus, muss der angegebene Pfad bereits durch Unraid oder Unassigned Devices gemountet sein.',
|
||||
remote: 'Adresse der Netzwerkfreigabe. SMB-Beispiel: //192.168.1.20/backup. NFS-Beispiel: 192.168.1.20:/volume1/backup.',
|
||||
@@ -117,15 +117,6 @@
|
||||
const id = (prefix) => `${prefix}-${crypto.randomUUID()}`;
|
||||
const repoName = (repoId) => state.config.repositories.find(r => r.id === repoId)?.name || repoId;
|
||||
const volatileLocation = (repo) => repo.type === 'local' && (/^\/tmp(?:\/|$)/.test(repo.location) || /^\/run(?:\/|$)/.test(repo.location));
|
||||
const repositoryFolderName = (name) => String(name || '')
|
||||
.normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/ß/g, 'ss').replace(/[^a-zA-Z0-9._-]+/g, '-')
|
||||
.replace(/^[._-]+|[._-]+$/g, '').slice(0, 64);
|
||||
const repositoryChildLocation = (base, name) => {
|
||||
const folder = repositoryFolderName(name);
|
||||
if (!folder) throw new Error('Der Repository-Name muss mindestens einen Buchstaben oder eine Zahl enthalten.');
|
||||
return `${String(base || '').replace(/\/+$/,'')}/${folder}`;
|
||||
};
|
||||
const tipAttr = (text) => text ? ` data-tooltip="${esc(text)}"` : '';
|
||||
const actionTooltip = (action, label) => {
|
||||
const [name] = action.split(':');
|
||||
@@ -429,12 +420,10 @@
|
||||
content.innerHTML = `<section class="bu-card"><h2>Repository ${repo.id?'bearbeiten':'erstellen'}</h2><form id="bu-repo-form" class="bu-form"><input type="hidden" name="id" value="${esc(repo.id||'')}"><input type="hidden" name="passwordRef" value="${esc(passwordRef)}"><input type="hidden" name="credentialRef" value="${esc(credentialRef)}"><input type="hidden" name="hasCredential" value="${repo.credentialRef?'true':'false'}">
|
||||
<label>Name<input name="name" required value="${esc(repo.name||'')}"></label><label>Typ<select name="type">${['local','smb','nfs','sftp'].map(v=>`<option value="${v}" ${v===repo.type?'selected':''}>${esc(typeLabels[v])}</option>`).join('')}</select></label>
|
||||
<div id="bu-repository-specific" class="wide"></div>
|
||||
${repo.id?'':'<div class="wide bu-info-card"><h3>Automatischer Repository-Unterordner</h3><p>URBM erstellt im gewählten Basisordner einen Unterordner aus dem Repository-Namen.</p><p>Endgültiger Speicherort: <strong id="bu-repository-final-location">Name und Basisordner eingeben</strong></p></div>'}
|
||||
<fieldset class="wide bu-fieldset"><legend>Restic-Verschlüsselung</legend><label>Repository-Passwort<input name="password" type="password" autocomplete="new-password" ${repo.id?'':'required'} placeholder="${repo.id?'Leer lassen, um das vorhandene Passwort zu behalten':'Ein starkes Repository-Passwort eingeben'}"></label><p class="bu-muted">Das Passwort wird von URBM geschützt gespeichert. ${repo.id?'Leer lassen, um das vorhandene Passwort zu behalten.':'Bewahre dieses Passwort sicher auf; ohne das Passwort können Backups nicht wiederhergestellt werden.'}</p></fieldset>
|
||||
<div class="wide bu-actions">${button('Speichern','submit-repo','primary')}${button('Abbrechen','cancel-form')}</div></form></section>`;
|
||||
enhanceTooltips(content);
|
||||
renderRepositorySpecific(repo.type || 'local', repo);
|
||||
updateRepositoryLocationPreview();
|
||||
}
|
||||
|
||||
function repositoryLocationField(label, value, placeholder) {
|
||||
@@ -445,25 +434,15 @@
|
||||
const host = document.getElementById('bu-repository-specific');
|
||||
if (!host) return;
|
||||
if (type === 'local') {
|
||||
host.innerHTML = `${repositoryLocationField(repo.id?'Lokaler Repository-Ordner':'Lokaler Basisordner',repo.location,'/mnt/user/backups')}<div id="bu-repository-browser"></div>`;
|
||||
host.innerHTML = `${repositoryLocationField('Lokaler Repository-Ordner',repo.location,'/mnt/user/backups/urbm')}<div id="bu-repository-browser"></div>`;
|
||||
renderRepositoryBrowser();
|
||||
} else if (type === 'smb' || type === 'nfs') {
|
||||
const managed = repo.mount?.managed === true;
|
||||
host.innerHTML = `<fieldset class="bu-fieldset"><legend>${type.toUpperCase()}-Ziel</legend><label class="bu-inline-check"><input name="managed" type="checkbox" ${managed?'checked':''}><span>URBM soll diese Freigabe einbinden</span></label><div class="bu-external-mount" ${managed?'hidden':''}>${repositoryLocationField(repo.id?'Bereits eingebundener Repository-Ordner':'Bereits eingebundener Basisordner',repo.location,'/mnt/remotes/backup-nas')}</div><div class="bu-managed-mount" ${managed?'':'hidden'}><div class="bu-form-grid"><label>Netzwerkfreigabe<input name="remote" placeholder="${type==='smb'?'//192.168.1.20/backup':'192.168.1.20:/volume1/backup'}" value="${esc(repo.mount?.remote||'')}"></label><label>${repo.id?'Einhängepunkt':'Basis-Einhängepunkt'}<input name="mountPoint" placeholder="/mnt/remotes/urbm-backup" value="${esc(repo.mount?.mountPoint||repo.location||'')}"></label>${type==='smb'?`<label class="wide">SMB-Zugangsdaten<textarea name="credential" placeholder="${repo.credentialRef?'Leer lassen, um vorhandene Zugangsdaten zu behalten':'username=backup password=geheim'}"></textarea></label>`:''}</div></div></fieldset>`;
|
||||
host.innerHTML = `<fieldset class="bu-fieldset"><legend>${type.toUpperCase()}-Ziel</legend><label class="bu-inline-check"><input name="managed" type="checkbox" ${managed?'checked':''}><span>URBM soll diese Freigabe einbinden</span></label><div class="bu-external-mount" ${managed?'hidden':''}>${repositoryLocationField('Bereits eingebundener Ordner',repo.location,'/mnt/remotes/backup-nas')}</div><div class="bu-managed-mount" ${managed?'':'hidden'}><div class="bu-form-grid"><label>Netzwerkfreigabe<input name="remote" placeholder="${type==='smb'?'//192.168.1.20/backup':'192.168.1.20:/volume1/backup'}" value="${esc(repo.mount?.remote||'')}"></label><label>Einhängepunkt<input name="mountPoint" placeholder="/mnt/remotes/urbm-backup" value="${esc(repo.mount?.mountPoint||repo.location||'')}"></label>${type==='smb'?`<label class="wide">SMB-Zugangsdaten<textarea name="credential" placeholder="${repo.credentialRef?'Leer lassen, um vorhandene Zugangsdaten zu behalten':'username=backup password=geheim'}"></textarea></label>`:''}</div></div></fieldset>`;
|
||||
} else {
|
||||
host.innerHTML = `<fieldset class="bu-fieldset"><legend>SFTP-Ziel</legend><div class="bu-form-grid">${repositoryLocationField(repo.id?'SFTP-Repository-Adresse':'SFTP-Basisadresse',repo.location,'backup@192.168.1.20:/srv/backups')}<label>Pfad zu SFTP known_hosts<input name="knownHostsPath" value="${esc(repo.options?.knownHostsPath||'/root/.ssh/known_hosts')}"></label><label class="wide">Privater SSH-Schlüssel (optional)<textarea name="credential" placeholder="${repo.credentialRef?'Leer lassen, um den vorhandenen Schlüssel zu behalten':'-----BEGIN OPENSSH PRIVATE KEY-----'}"></textarea></label></div></fieldset>`;
|
||||
host.innerHTML = `<fieldset class="bu-fieldset"><legend>SFTP-Ziel</legend><div class="bu-form-grid">${repositoryLocationField('SFTP-Repository-Adresse',repo.location,'backup@192.168.1.20:/srv/backups/urbm')}<label>Pfad zu SFTP known_hosts<input name="knownHostsPath" value="${esc(repo.options?.knownHostsPath||'/root/.ssh/known_hosts')}"></label><label class="wide">Privater SSH-Schlüssel (optional)<textarea name="credential" placeholder="${repo.credentialRef?'Leer lassen, um den vorhandenen Schlüssel zu behalten':'-----BEGIN OPENSSH PRIVATE KEY-----'}"></textarea></label></div></fieldset>`;
|
||||
}
|
||||
enhanceTooltips(host);
|
||||
updateRepositoryLocationPreview();
|
||||
}
|
||||
|
||||
function updateRepositoryLocationPreview() {
|
||||
const form=document.getElementById('bu-repo-form'); const output=document.getElementById('bu-repository-final-location');
|
||||
if(!form||!output||form.elements.id.value) return;
|
||||
const type=form.elements.type.value; const managed=(type==='smb'||type==='nfs')&&form.elements.managed?.checked;
|
||||
const base=managed?form.elements.mountPoint?.value:form.elements.location?.value;
|
||||
try { output.textContent=base&&form.elements.name.value?repositoryChildLocation(base,form.elements.name.value):'Name und Basisordner eingeben'; }
|
||||
catch(error) { output.textContent=error.message; }
|
||||
}
|
||||
|
||||
function renderRepositoryBrowser() {
|
||||
@@ -576,7 +555,7 @@
|
||||
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; updateRepositoryLocationPreview(); return; }
|
||||
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(); }
|
||||
@@ -605,10 +584,8 @@
|
||||
let location=managed?value('mountPoint'):value('location');
|
||||
if(!location) throw new Error(managed?'Ein Einhängepunkt ist erforderlich.':'Ein Repository-Speicherort ist erforderlich.');
|
||||
if(managed&&!value('remote')) throw new Error('Für eine verwaltete Einbindung ist eine Netzwerkfreigabe erforderlich.');
|
||||
const isNew=!value('id'); const baseLocation=location;
|
||||
if(isNew) location=repositoryChildLocation(baseLocation,value('name'));
|
||||
const usesCredential=type==='sftp'||(type==='smb'&&managed); const credentialRef=usesCredential&&(value('hasCredential')==='true'||Boolean(f.get('credential')))?value('credentialRef'):'';
|
||||
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:baseLocation}:null,options:type==='sftp'?{knownHostsPath:value('knownHostsPath')}: {}};
|
||||
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(!value('id')&&!f.get('password')) throw new Error('Für ein neues Repository ist ein Passwort erforderlich.');
|
||||
if (volatileLocation(repo) && !confirm('WARNUNG: Dieses Ziel liegt unter /tmp oder /run und geht nach einem Unraid-Neustart verloren. Trotzdem speichern?')) return;
|
||||
if(f.get('password')) await api(`/v1/secrets/${encodeURIComponent(repo.passwordRef)}`,{method:'PUT',body:JSON.stringify({type:'restic-password',value:f.get('password')})});
|
||||
@@ -637,7 +614,6 @@
|
||||
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;
|
||||
updateRepositoryLocationPreview();
|
||||
}
|
||||
if (e.target.name === 'scheduleMode') {
|
||||
const weekdays=e.target.closest('.bu-schedule')?.querySelector('.bu-weekdays');
|
||||
@@ -660,9 +636,6 @@
|
||||
renderWorkloadBrowser(document.querySelector('#bu-job-form [name="type"]')?.value||'docker');
|
||||
}
|
||||
});
|
||||
content.addEventListener('input', e => {
|
||||
if (e.target.closest('#bu-repo-form') && ['name','location','mountPoint'].includes(e.target.name)) updateRepositoryLocationPreview();
|
||||
});
|
||||
content.addEventListener('submit', e => {
|
||||
const form = e.target.closest('form');
|
||||
if (!form) return;
|
||||
|
||||
Reference in New Issue
Block a user