Add drag-and-drop archive upload with progress to the backup page
This commit is contained in:
1 parent
f60339a60f
commit
ac38227dac
1 file changed
+157
-2
@@ -106,6 +106,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-upload"></i> Upload an archive</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small">
|
||||
Drop a <code>.zip</code> taken from another LIM instance here, then restore it
|
||||
from the table below. The file streams straight to
|
||||
<code>{{ backup_folder }}</code> — it is not held in memory, and the
|
||||
50MB limit on instruction uploads does not apply.
|
||||
{% if max_upload %}Ceiling: {{ max_upload }} bytes.{% endif %}
|
||||
</p>
|
||||
|
||||
<div id="dropZone" class="border rounded p-4 text-center mb-3"
|
||||
style="border-style: dashed !important; cursor: pointer;">
|
||||
<i class="bi bi-file-earmark-zip fs-1 d-block mb-2 text-muted"></i>
|
||||
<span class="text-muted">Drop a backup archive here, or click to choose one</span>
|
||||
<input type="file" id="archiveInput" accept=".zip,application/zip" hidden>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-2 d-none" id="chosenWrap">
|
||||
<code id="chosenName" class="flex-grow-1"></code>
|
||||
<span class="text-muted small" id="chosenSize"></span>
|
||||
<button type="button" class="btn btn-sm btn-danger" id="startUpload">
|
||||
<i class="bi bi-upload"></i> Upload
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 d-none" id="uploadProgressWrap">
|
||||
<div class="progress" style="height: 22px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
id="uploadBar" role="progressbar" style="width: 0%;">0%</div>
|
||||
</div>
|
||||
<div class="small text-muted mt-2" id="uploadNote"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 d-none" id="uploadResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing backups -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
@@ -201,8 +242,8 @@
|
||||
<div class="card-body">
|
||||
<ol class="mb-2">
|
||||
<li>Create a backup here and download the <code>.zip</code>.</li>
|
||||
<li>Copy it into the new instance's backup folder (or bind-mount it).</li>
|
||||
<li>On the new instance run:
|
||||
<li>Upload it to the new instance with the box above (or copy it into the backup folder).</li>
|
||||
<li>Restore it from the table — or, on a fresh instance with no admin user yet:
|
||||
<pre class="bg-light p-2 mt-2 mb-2"><code>flask backup verify lim-backup-....zip
|
||||
flask backup restore lim-backup-....zip --mode fresh</code></pre>
|
||||
</li>
|
||||
@@ -354,6 +395,120 @@ flask backup restore lim-backup-....zip --mode fresh</code></pre>
|
||||
});
|
||||
});
|
||||
|
||||
// ---- upload ----------------------------------------------------------
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
const archiveInput = document.getElementById('archiveInput');
|
||||
const chosenWrap = document.getElementById('chosenWrap');
|
||||
const chosenName = document.getElementById('chosenName');
|
||||
const chosenSize = document.getElementById('chosenSize');
|
||||
const startUpload = document.getElementById('startUpload');
|
||||
const upWrap = document.getElementById('uploadProgressWrap');
|
||||
const upBar = document.getElementById('uploadBar');
|
||||
const upNote = document.getElementById('uploadNote');
|
||||
const upResult = document.getElementById('uploadResult');
|
||||
let chosenFile = null;
|
||||
|
||||
function humanSize(bytes) {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes, i = 0;
|
||||
while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; }
|
||||
return size.toFixed(1) + ' ' + units[i];
|
||||
}
|
||||
|
||||
function chooseFile(file) {
|
||||
if (!file) { return; }
|
||||
if (!file.name.toLowerCase().endsWith('.zip')) {
|
||||
upResult.innerHTML = '<div class="alert alert-warning mb-0">That is not a .zip archive.</div>';
|
||||
upResult.classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
chosenFile = file;
|
||||
chosenName.textContent = file.name;
|
||||
chosenSize.textContent = humanSize(file.size);
|
||||
chosenWrap.classList.remove('d-none');
|
||||
upResult.classList.add('d-none');
|
||||
}
|
||||
|
||||
dropZone.addEventListener('click', () => archiveInput.click());
|
||||
archiveInput.addEventListener('change', () => chooseFile(archiveInput.files[0]));
|
||||
['dragenter', 'dragover'].forEach(evt =>
|
||||
dropZone.addEventListener(evt, e => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-light');
|
||||
}));
|
||||
['dragleave', 'drop'].forEach(evt =>
|
||||
dropZone.addEventListener(evt, e => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-light');
|
||||
}));
|
||||
dropZone.addEventListener('drop', e => chooseFile(e.dataTransfer.files[0]));
|
||||
|
||||
startUpload.addEventListener('click', function () {
|
||||
if (!chosenFile) { return; }
|
||||
startUpload.disabled = true;
|
||||
upWrap.classList.remove('d-none');
|
||||
upResult.classList.add('d-none');
|
||||
upBar.classList.add('progress-bar-animated');
|
||||
upBar.classList.remove('bg-success', 'bg-danger');
|
||||
upBar.style.width = '0%';
|
||||
upBar.textContent = '0%';
|
||||
upNote.textContent = 'Uploading ' + chosenFile.name + '...';
|
||||
|
||||
// Raw body, not multipart: keeps the whole file out of Flask's
|
||||
// form parser (and its 50MB ceiling) and off the temp disk.
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', "{{ url_for('backup.upload') }}?filename=" +
|
||||
encodeURIComponent(chosenFile.name));
|
||||
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
||||
|
||||
xhr.upload.addEventListener('progress', function (e) {
|
||||
if (!e.lengthComputable) { return; }
|
||||
const pct = Math.round((e.loaded / e.total) * 100);
|
||||
upBar.style.width = pct + '%';
|
||||
upBar.textContent = pct + '%';
|
||||
upNote.textContent = humanSize(e.loaded) + ' of ' + humanSize(e.total);
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', function () {
|
||||
upBar.classList.remove('progress-bar-animated');
|
||||
startUpload.disabled = false;
|
||||
let data = {};
|
||||
try { data = JSON.parse(xhr.responseText); } catch (err) { /* below */ }
|
||||
|
||||
if (xhr.status === 200) {
|
||||
upBar.classList.add('bg-success');
|
||||
upBar.style.width = '100%';
|
||||
upBar.textContent = '100%';
|
||||
upNote.textContent = 'Validated as a LIM backup.';
|
||||
const c = data.counts || {};
|
||||
upResult.innerHTML = '<div class="alert alert-success mb-0">' +
|
||||
'<strong>Uploaded:</strong> <code>' + data.filename + '</code> (' +
|
||||
data.size_human + ')<br><span class="small">' +
|
||||
(c.sets || 0) + ' sets, ' + (c.instructions || 0) + ' instructions, ' +
|
||||
(c.extra_files || 0) + ' extra files' +
|
||||
(data.source ? ' — from ' + data.source : '') +
|
||||
'</span><br><a href="{{ url_for('backup.index') }}" class="alert-link small">' +
|
||||
'Reload to restore it</a></div>';
|
||||
} else {
|
||||
upBar.classList.add('bg-danger');
|
||||
upResult.innerHTML = '<div class="alert alert-danger mb-0"><strong>Upload failed:</strong> ' +
|
||||
(data.error || ('HTTP ' + xhr.status)) + '</div>';
|
||||
}
|
||||
upResult.classList.remove('d-none');
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', function () {
|
||||
upBar.classList.remove('progress-bar-animated');
|
||||
upBar.classList.add('bg-danger');
|
||||
startUpload.disabled = false;
|
||||
upResult.innerHTML = '<div class="alert alert-danger mb-0">' +
|
||||
'Upload failed. If you are behind a reverse proxy, check its max body size.</div>';
|
||||
upResult.classList.remove('d-none');
|
||||
});
|
||||
|
||||
xhr.send(chosenFile);
|
||||
});
|
||||
|
||||
// Verify buttons
|
||||
document.querySelectorAll('.verify-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
|
||||
Reference in new issue
Block a user