Restore native LTX media mounts with a restricted shared-directory policy
This commit is contained in:
1 parent
ea33527233
commit
21f11eac57
3 files changed
+13
-3
No files matched your search
+1
-1
@@ -9,7 +9,7 @@ window.BackupUI=(()=>{
|
||||
view.querySelector('#backup-import').onsubmit=async e=>{e.preventDefault();if(pending)return;pending=true;message.textContent='Backup entschlüsseln und Quellen prüfen …';try{const file=view.querySelector('#backup-file').files[0];if(!file||file.size>64*1024**2)throw Error('Backup-Datei fehlt oder ist größer als 64 MiB.');const r=await fetch('/api/v1/backup/inspect',{method:'POST',headers:{'Content-Type':'application/octet-stream','X-Athena-Deck':'1','X-Backup-Passphrase':btoa(String.fromCharCode(...new TextEncoder().encode(view.querySelector('#backup-import-password').value)))},body:file});const data=await r.json();if(!r.ok)throw Error(data.error);preview=data;view.querySelector('#backup-import-password').value='';showPreview(panel,data,message);message.textContent='Backup geprüft. Details und Zusatzdienste auswählen.';}catch(e){message.textContent=e.message;}finally{pending=false;}};
|
||||
poll(view);
|
||||
}
|
||||
function showPreview(panel,p,message){panel.innerHTML=`<h3>Wiederherstellungsplan</h3><p>${p.profiles.length} Profile · ${p.models.length} Modelldateien · ${size(p.download_bytes)} fehlende Gewichte · ${p.wireguard?'WireGuard enthalten':'Kein Deck-WireGuard'}</p><details><summary>Modelle und Profile anzeigen</summary><ul>${p.models.map(m=>`<li>${esc(m.name)} · ${size(m.bytes)} · ${m.present?'vorhanden, wird geprüft':'wird heruntergeladen'}</li>`).join('')}</ul><p>${p.profiles.map(esc).join(' · ')}</p></details><h3>Zusatzdienste</h3>${p.services.length?p.services.map(c=>`<label><input type="checkbox" class="backup-service" value="${esc(c.name)}" ${c.restorable?'checked':'disabled'}> ${esc(c.name)} · ${esc(c.registry||c.image)} ${c.build_recipe?'· wird aus festem Rezept gebaut':''}${!c.restorable?'· keine Wiederherstellungsquelle':''}</label>`).join(''):'<p>Keine zugeordneten Container.</p>'}<h3>Laufzeiten</h3><p>llama.cpp: ${esc(p.runtimes.llama?.commit||'nicht eingerichtet')} · ComfyUI: ${p.runtimes.image?'vorhandene wiederverwenden / fehlende installieren':'nicht benötigt'} · TTS: ${p.runtimes.tts?'wiederherstellen':'nicht benötigt'}</p>${p.warnings.map(w=>`<p class="note">${esc(w)}</p>`).join('')}${p.blockers.map(w=>`<p role="alert">${esc(w)}</p>`).join('')}<label><input type="checkbox" id="backup-credentials" checked> Oberflächenkennwort und API-Zugang aus Backup wiederherstellen</label><p class="small">Danach gilt das bisherige Kennwort des Backups. Der aktuelle Zugang bleibt erhalten, wenn der Haken entfernt wird.</p><label><input type="checkbox" id="backup-confirm"> Einstellungen ersetzen und ausgewählte Downloads/Builds automatisch ausführen. Lokaler Rückfallstand wird vorher gesichert.</label><p><button id="backup-restore" ${p.blockers.length?'disabled':''}>Wiederherstellung starten</button></p>`;
|
||||
function showPreview(panel,p,message){panel.innerHTML=`<h3>Wiederherstellungsplan</h3><p>${p.profiles.length} Profile · ${p.models.length} Modelldateien · ${size(p.download_bytes)} fehlende Gewichte · ${p.wireguard?'WireGuard enthalten':'Kein Deck-WireGuard'}</p><details><summary>Modelle und Profile anzeigen</summary><ul>${p.models.map(m=>`<li>${esc(m.name)} · ${size(m.bytes)} · ${m.present?'vorhanden, wird geprüft':'wird heruntergeladen'}</li>`).join('')}</ul><p>${p.profiles.map(esc).join(' · ')}</p></details><h3>Zusatzdienste</h3>${p.services.length?p.services.map(c=>`<label><input type="checkbox" class="backup-service" value="${esc(c.name)}" ${c.restorable?'checked':'disabled'}> ${esc(c.name)} · ${esc(c.registry||c.image)} ${c.build_recipe?'· wird aus festem Rezept gebaut':''}${!c.restorable?'· keine Wiederherstellungsquelle':''}</label>`).join(''):'<p>Keine zugeordneten Container.</p>'}<h3>Laufzeiten</h3><p>llama.cpp: ${esc(p.runtimes.llama?.commit||'nicht eingerichtet')} · ComfyUI: ${p.runtimes.image?'vorhandene wiederverwenden / fehlende installieren':'nicht benötigt'} · TTS: ${p.runtimes.tts?'wiederherstellen':'nicht benötigt'} · LTX Original: ${p.runtimes.ltx_original?'wiederherstellen':'nicht benötigt'}</p>${p.warnings.map(w=>`<p class="note">${esc(w)}</p>`).join('')}${p.blockers.map(w=>`<p role="alert">${esc(w)}</p>`).join('')}<label><input type="checkbox" id="backup-credentials" checked> Oberflächenkennwort und API-Zugang aus Backup wiederherstellen</label><p class="small">Danach gilt das bisherige Kennwort des Backups. Der aktuelle Zugang bleibt erhalten, wenn der Haken entfernt wird.</p><label><input type="checkbox" id="backup-confirm"> Einstellungen ersetzen und ausgewählte Downloads/Builds automatisch ausführen. Lokaler Rückfallstand wird vorher gesichert.</label><p><button id="backup-restore" ${p.blockers.length?'disabled':''}>Wiederherstellung starten</button></p>`;
|
||||
panel.querySelector('#backup-restore').onclick=async()=>{if(pending)return;if(!panel.querySelector('#backup-confirm').checked){message.textContent='Bitte den angezeigten Wiederherstellungsplan bestätigen.';return;}pending=true;try{await request('restore',{id:p.id,services:[...panel.querySelectorAll('.backup-service:checked')].map(x=>x.value),confirm:true,restore_credentials:panel.querySelector('#backup-credentials').checked});panel.innerHTML='';preview=null;message.textContent='Wiederherstellung gestartet.';}catch(e){message.textContent=e.message;}finally{pending=false;}};
|
||||
}
|
||||
async function poll(view){if(!view.querySelector('#backup-page'))return;const panel=view.querySelector('#backup-progress');try{const r=await fetch('/api/v1/backup');if(!r.ok)throw Error('Restore-Status nicht erreichbar.');const {job,preview:p,checkpoints=[]}=await r.json();const saved=view.querySelector('#backup-checkpoints');saved.innerHTML='<h2>Lokale Rückfallstände</h2>'+checkpoints.map(name=>`<p><button data-checkpoint="${esc(name)}">${esc(name)} prüfen</button></p>`).join('');saved.querySelectorAll('[data-checkpoint]').forEach(b=>b.onclick=async()=>{try{const r=await request('checkpoint',{name:b.dataset.checkpoint});const p=await r.json();preview=p;showPreview(view.querySelector('#backup-preview'),p,view.querySelector('#backup-message'));}catch(e){view.querySelector('#backup-message').textContent=e.message;}});if(p&&!preview){preview=p;showPreview(view.querySelector('#backup-preview'),p,view.querySelector('#backup-message'));}if(!job){panel.innerHTML='<h2>Wiederherstellung</h2><p>Noch kein Restore gestartet.</p>';}else{const running=job.state==='running';panel.innerHTML=`<h2>${job.state==='complete'?'Alles wiederhergestellt':job.state==='partial'?'Wiederherstellung mit offenen Punkten':'Wiederherstellung · '+esc(job.state)}</h2><p>${esc(job.phase)}</p><progress max="${job.total}" value="${job.completed}"></progress><p>${job.completed}/${job.total} Schritte</p>${job.current?.total?`<progress max="${job.current.total}" value="${job.current.bytes||0}"></progress><p>${size(job.current.bytes||0)} / ${size(job.current.total)}${job.current.bytes_per_second?' · '+(job.current.bytes_per_second/1024**2).toFixed(1)+' MiB/s':''}${job.current.eta_seconds!=null?' · noch etwa '+Math.ceil(job.current.eta_seconds/60)+' Minuten':''}</p>`:''}${running?'<button id="backup-cancel">Wiederherstellung abbrechen</button>':''}<ul>${job.items.map(x=>`<li><strong>${esc(x.name)}</strong> · ${esc(x.state)}<br>${esc(x.message)}</li>`).join('')}</ul>`;if(running)panel.querySelector('#backup-cancel').onclick=async()=>{try{await request('cancel',{});view.querySelector('#backup-message').textContent='Abbruch angefordert; laufender Container-Schritt wird sicher beendet.';}catch(e){view.querySelector('#backup-message').textContent=e.message;}};if(job.state==='complete'&&jobId!==job.id){jobId=job.id;applyDeckTheme(job.theme);}}}catch(e){view.querySelector('#backup-message').textContent=e.message;}setTimeout(()=>poll(view),1500);}
|
||||
|
||||
@@ -89,7 +89,9 @@ def validate(c):
|
||||
targets.add(target)
|
||||
if m.get('index')!=i or type(m.get('read_only')) is not bool or type(m.get('was_file')) is not bool:raise ValueError('Ungültige Volume-Einstellung.')
|
||||
rel=m.get('deck_path')
|
||||
if rel is not None and (rel not in ('models','video/comfy-work/models','video/comfy-client-token') or not m['read_only']):raise ValueError('Deck-Dateien dürfen nur als bekannte, lesende Modell-/Token-Volumes eingebunden werden.')
|
||||
media={'video/original-work/remote-inputs/deskweb':('/data/inputs',False),'video/original-work/outputs':('/data/outputs',True)}
|
||||
native_media=rel in media and c['name']=='ltx-deskweb' and (target,m['read_only'])==media[rel] and not m['was_file']
|
||||
if rel is not None and not native_media and (rel not in ('models','video/comfy-work/models','video/comfy-client-token') or not m['read_only']):raise ValueError('Deck-Volume nicht freigegeben; nur bekannte Modell-/Tokenpfade und LTX-Medienordner erlaubt.')
|
||||
for name,data in m.get('files',{}).items():
|
||||
if name not in ('file','Settings.fds','Backends.fds'):raise ValueError('Unbekannte Konfigurationsdatei.')
|
||||
if not isinstance(data,str) or len(base64.b64decode(data,validate=True))>MAX_FILE:raise ValueError('Ungültige Konfigurationsdatei.')
|
||||
@@ -150,7 +152,10 @@ def restore(manager,c):
|
||||
if m['deck_path']=='video/comfy-client-token' and not source.exists():
|
||||
import secrets
|
||||
source.parent.mkdir(parents=True,exist_ok=True);source.write_text(secrets.token_urlsafe(48)+'\n');source.chmod(0o600);os.chown(source,65534,65534)
|
||||
elif m['deck_path']!='video/comfy-client-token':source.mkdir(parents=True,exist_ok=True)
|
||||
elif m['deck_path']!='video/comfy-client-token':
|
||||
source.mkdir(parents=True,exist_ok=True)
|
||||
if m['deck_path'].startswith('video/original-work/'):
|
||||
is_input=m['deck_path'].endswith('/deskweb');os.chown(source,1000 if is_input else 65534,65534);source.chmod(0o2770 if is_input else 0o2750)
|
||||
elif m['was_file']:
|
||||
if 'file' not in m['files']:raise ValueError('Benötigte Volume-Datei fehlt im Backup.')
|
||||
source.write_bytes(base64.b64decode(m['files']['file']));source.chmod(0o600)
|
||||
|
||||
@@ -82,6 +82,11 @@ class ContainerTests(unittest.TestCase):
|
||||
for change in (lambda x:x.update(unsupported=True),lambda x:x.update(network='host'),lambda x:x['ports']['8000/tcp'][0].update(HostIp='0.0.0.0'),lambda x:x.update(mounts=[dict(index=0,target='/var/run/docker.sock',deck_path=None,read_only=True,was_file=True,files={})])):
|
||||
bad=copy.deepcopy(c);change(bad)
|
||||
with self.assertRaises(ValueError):validate_container(bad)
|
||||
def test_only_deskweb_can_mount_its_specific_writable_media_folder(self):
|
||||
c=self.config();c.update(name='ltx-deskweb',mounts=[dict(index=0,target='/data/inputs',deck_path='video/original-work/remote-inputs/deskweb',read_only=False,was_file=False,files={})]);validate_container(c)
|
||||
for change in (lambda x:x.update(name='other-ui'),lambda x:x['mounts'][0].update(deck_path='models'),lambda x:x['mounts'][0].update(deck_path='video/original-work'),lambda x:x['mounts'][0].update(target='/data/outputs')):
|
||||
bad=copy.deepcopy(c);change(bad)
|
||||
with self.assertRaises(ValueError):validate_container(bad)
|
||||
def test_never_replaces_existing_foreign_container(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
manager=Mock(state=Path(d));(Path(d)/'backup-policy.json').write_text(json.dumps({'deck_state':d}))
|
||||
|
||||
Reference in new issue
Block a user