vm-bench/frontend/templates/scp.html
Claus Lohmar 837e674195 feat: SCP Pull page for large files from remote servers
New /scp page with form for host, user, password, remote path.
Runs scp via sshpass in background with progress polling.
Files go to /mnt/converter/in/{session_id}/ for multi-user isolation.

- Added sshpass + openssh-client to frontend installer
- scp.html template with full form + progress bar
- POST /scp/start — launches background SCP via sshpass -e
- GET /scp/progress/{sid}/{file} — polls file size + speed
- 'SCP Pull (+10 GB)' button on start page
2026-07-22 05:52:01 +00:00

161 lines
5.5 KiB
HTML

{% extends "base.html" %}
{% block content %}
<div class="panel" id="scp-form-panel">
<h2>SCP Pull — Fetch File from Remote Server</h2>
<p class="confirm-text">
Pull a disk image directly from a customer server via SCP.
No laptop middle-hop — files land directly in the staging area.
<strong>Supports files of any size.</strong>
</p>
<form id="scp-form" onsubmit="startScpPull(event)">
<div class="row">
<div class="form-group">
<label>Remote Host</label>
<input type="text" name="scp_host" required
placeholder="10.0.0.50 or server.example.com">
</div>
<div class="form-group">
<label>SSH Port</label>
<input type="number" name="scp_port" value="22" min="1" max="65535">
</div>
</div>
<div class="row">
<div class="form-group">
<label>Username</label>
<input type="text" name="scp_user" required placeholder="root">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" name="scp_pass" required placeholder="••••••••">
</div>
</div>
<div class="form-group">
<label>Remote File Path *</label>
<input type="text" name="scp_path" required
placeholder="/mnt/vmware/exchange-server.vmdk">
<span class="hint">Full path to the disk image or archive on the remote server.</span>
</div>
<div class="row">
<div class="form-group">
<label>VM Name *</label>
<input type="text" id="scp-vm-name" name="vm_name" required
placeholder="e.g. exchange-prod">
</div>
<div class="form-group">
<label>VM ID *</label>
<input type="number" id="scp-vmid" name="vmid" required
placeholder="e.g. 21050" min="21000" max="21100">
</div>
</div>
<button type="submit" id="scp-start-btn">Start SCP Pull</button>
</form>
<!-- Progress area -->
<div id="scp-status" class="hidden" style="margin-top:1rem;">
<div id="scp-phase-label" style="margin-bottom:0.5rem;"></div>
<div class="progress-bar">
<div class="progress-fill" id="scp-progress-fill" style="width:0%">0%</div>
</div>
<div id="scp-error"></div>
</div>
</div>
<!-- Analysis result (populated after pull completes) -->
<div id="scp-analysis-section"></div>
<script>
// Session ID for multi-user isolation
const SESSION_ID = '{{ session_id }}';
async function startScpPull(e) {
e.preventDefault();
const btn = document.getElementById('scp-start-btn');
const status = document.getElementById('scp-status');
const analysis = document.getElementById('scp-analysis-section');
btn.disabled = true;
analysis.innerHTML = '';
status.classList.remove('hidden');
document.getElementById('scp-error').textContent = '';
const formData = new FormData(e.target);
formData.append('session_id', SESSION_ID);
setScpPhase('Connecting to remote server...', 0, true);
try {
const resp = await fetch('/scp/start', { method: 'POST', body: formData });
const data = await resp.json();
if (data.phase === 'error') {
setScpPhase('SCP pull failed', 0, false);
showScpError(data.error);
btn.disabled = false;
return;
}
// Poll progress
const filename = data.filename;
setScpPhase('Pulling file from remote server...', 0, true);
for (;;) {
await new Promise(r => setTimeout(r, 2000));
try {
const pr = await fetch('/scp/progress/' + SESSION_ID + '/' + encodeURIComponent(filename));
const pdata = await pr.json();
if (pdata.phase === 'complete') {
setScpPhase('Pull complete (' + (pdata.file_size_gb || 0) + ' GiB)', 100, false);
await new Promise(r => setTimeout(r, 500));
// Run analysis
setScpPhase('Step 2/2: Analysing source image...', 0, true);
const fd = new FormData();
fd.append('vmid', document.getElementById('scp-vmid').value);
fd.append('filename', filename);
fd.append('vm_name', document.getElementById('scp-vm-name').value.trim());
fd.append('session_id', SESSION_ID);
const ar = await fetch('/session/analyze', { method: 'POST', body: fd });
document.getElementById('scp-analysis-section').innerHTML = await ar.text();
document.getElementById('scp-status').classList.add('hidden');
btn.disabled = false;
return;
}
if (pdata.phase === 'error') {
setScpPhase('SCP pull failed', 0, false);
showScpError(pdata.error);
btn.disabled = false;
return;
}
if (pdata.phase === 'pulling') {
const gb = (pdata.file_size_gb || 0).toFixed(1);
let label = 'Pulling file... ' + gb + ' GiB';
if (pdata.speed_mbps) label += ' (' + pdata.speed_mbps + ' MB/s)';
setScpPhase(label, 0, true);
}
} catch (_) {}
}
} catch (err) {
setScpPhase('SCP pull failed', 0, false);
showScpError('Network error: ' + err.message);
btn.disabled = false;
}
}
function setScpPhase(label, pct, spinner) {
const el = document.getElementById('scp-phase-label');
if (el) el.innerHTML = (spinner ? '<span class="spinner"></span> ' : '') + label;
const fill = document.getElementById('scp-progress-fill');
if (fill) { fill.style.width = pct + '%'; fill.textContent = pct > 0 ? pct + '%' : ''; }
}
function showScpError(msg) {
const el = document.getElementById('scp-error');
if (el) el.innerHTML += '<div class="error" style="margin-top:0.5rem;">' + msg + '</div>';
}
</script>
{% endblock %}