controller: .fab browser download — the existing export pipeline staged under the data dir + a guarded streaming exit (estimate-first, io.Copy, post-stream cleanup, 1h TTL sweep); portability framing (decision 3); traversal guard red-proven, export→import round-trip + corrupt-bundle refusal unit-proven
Claude-Session: https://claude.ai/code/session_01GzammAMzsJTgpQHqxwM2bC
This commit is contained in:
@@ -89,10 +89,99 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
||||
<!-- Hordozható mentéscsomag (.fab) — v0.124.0, decision 3: PORTABILITY, not a backup tier (no
|
||||
scheduling, no status surface; point-in-time framing everywhere). The download reuses the
|
||||
EXISTING export pipeline (same producer as a drive export → byte-identical bundle) staged
|
||||
under the data dir, then streams through a guarded endpoint. Import stays drive-scan. -->
|
||||
<div class="backup-section-card">
|
||||
<h3>Hordozható mentéscsomag (.fab)</h3>
|
||||
<p class="form-hint" style="margin:-0.25rem 0 1rem">Hordozható pillanatfelvétel — bárhol tárolhatod, és bármikor visszatöltheted egy meghajtóról. A folyamatos védelmet az 1–3. szintű mentés adja.</p>
|
||||
{{if .OffboxApps}}
|
||||
<div class="form-row" style="max-width:420px"><label>Jelszavas titkosítás (opcionális)</label>
|
||||
<input type="password" id="fab-dl-password" class="form-input" autocomplete="new-password" placeholder="Üresen hagyva a csomag titkosítatlan">
|
||||
</div>
|
||||
<div class="storage-paths-list">
|
||||
{{range .OffboxApps}}
|
||||
<div class="storage-path-item">
|
||||
<div class="storage-path-header">
|
||||
<div class="storage-path-info"><span class="storage-path-label">{{.DisplayName}}</span></div>
|
||||
<div class="storage-path-actions">
|
||||
<button type="button" class="btn btn-xs btn-outline fab-dl-btn" data-stack="{{.Name}}" onclick="fabDownload(this)">Letöltés (.fab)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="schedule-actions" style="margin-top:.75rem">
|
||||
<button type="button" class="btn btn-xs btn-outline" id="fab-dl-all" onclick="fabDownloadAll()">Összes letöltése (egyenként)</button>
|
||||
<span class="form-hint" style="margin-left:.5rem">A csomagok egyenként készülnek és töltődnek le — pillanatfelvétel a mostani állapotról.</span>
|
||||
</div>
|
||||
<div id="fab-dl-status" class="form-hint" style="margin-top:.5rem"></div>
|
||||
{{else}}
|
||||
<p class="form-hint">Nincs telepített alkalmazás.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{end}}
|
||||
|
||||
<script>
|
||||
{{template "restore_banner_js"}}
|
||||
|
||||
// .fab download flow (v0.124.0): estimate → inline confirm (size shown BEFORE starting) →
|
||||
// start (the EXISTING async export, staging dest) → poll → browser GET (guarded stream; the
|
||||
// server removes the staged bundle after the stream). The batch runs apps ONE AT A TIME.
|
||||
var fabQueue = [];
|
||||
function fabSetStatus(msg){ document.getElementById('fab-dl-status').textContent = msg; }
|
||||
function fabPassword(){ var el = document.getElementById('fab-dl-password'); return el ? el.value : ''; }
|
||||
function fabDownload(btn){
|
||||
var stack = btn.getAttribute('data-stack');
|
||||
fetch('/api/export/download/estimate?stack=' + encodeURIComponent(stack))
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(j){
|
||||
if (!j.ok) { fabSetStatus('Hiba: ' + (j.error || 'a becslés sikertelen')); return; }
|
||||
var size = (j.data && j.data.total_size_human) || '?';
|
||||
felhomConfirm(btn, 'A csomag becsült mérete: ' + size + '. Elindítod a letöltést? (Pillanatfelvétel a mostani állapotról.)', function(){
|
||||
fabStart(stack, null);
|
||||
});
|
||||
})
|
||||
.catch(function(){ fabSetStatus('Hiba: a becslés nem érhető el.'); });
|
||||
}
|
||||
function fabStart(stack, next){
|
||||
fetch('/api/export/download/start', {method:'POST', headers:Object.assign({'Content-Type':'application/json'}, csrfHeaders()), body: JSON.stringify({stack_name: stack, password: fabPassword()})})
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(j){
|
||||
if (!j.ok) { fabSetStatus('Hiba (' + stack + '): ' + (j.error || 'az export nem indult el')); if (next) next(); return; }
|
||||
fabSetStatus('Csomag készítése: ' + stack + '…');
|
||||
fabPoll(stack, next);
|
||||
})
|
||||
.catch(function(){ fabSetStatus('Hiba: az export nem indult el.'); if (next) next(); });
|
||||
}
|
||||
function fabPoll(stack, next){
|
||||
var iv = setInterval(function(){
|
||||
fetch('/api/export/status').then(function(r){ return r.json(); }).then(function(j){
|
||||
if (!j || j.running || !j.done) return; // keep polling until the job reports done
|
||||
clearInterval(iv);
|
||||
if (j.error) { fabSetStatus('Hiba (' + stack + '): ' + j.error); if (next) next(); return; }
|
||||
var base = (j.output_path || '').split('/').pop();
|
||||
if (!base) { fabSetStatus('Hiba: a csomag útvonala hiányzik.'); if (next) next(); return; }
|
||||
fabSetStatus('Letöltés: ' + base + (j.output_size ? ' (' + j.output_size + ')' : ''));
|
||||
window.location.href = '/api/export/download?file=' + encodeURIComponent(base);
|
||||
if (next) setTimeout(next, 3000); // let the stream begin before the next export starts
|
||||
}).catch(function(){});
|
||||
}, 2000);
|
||||
}
|
||||
function fabDownloadAll(){
|
||||
var btns = document.querySelectorAll('.fab-dl-btn');
|
||||
fabQueue = Array.prototype.map.call(btns, function(b){ return b.getAttribute('data-stack'); });
|
||||
fabRunQueue();
|
||||
}
|
||||
function fabRunQueue(){
|
||||
var stack = fabQueue.shift();
|
||||
if (!stack) { fabSetStatus('Minden csomag elkészült.'); return; }
|
||||
fabStart(stack, fabRunQueue);
|
||||
}
|
||||
|
||||
// Restore section
|
||||
var huDays = ['vasárnap', 'hétfő', 'kedd', 'szerda', 'csütörtök', 'péntek', 'szombat'];
|
||||
function formatSnapshot(s) {
|
||||
|
||||
Reference in New Issue
Block a user