|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>File Uploader</title> |
|
|
|
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"> |
|
|
<style> |
|
|
body { |
|
|
padding-top: 40px; |
|
|
padding-bottom: 40px; |
|
|
background-color: #f5f5f5; |
|
|
} |
|
|
.container { |
|
|
max-width: 600px; |
|
|
} |
|
|
.upload-form { |
|
|
background-color: #fff; |
|
|
padding: 30px; |
|
|
border-radius: 8px; |
|
|
box-shadow: 0 0 10px rgba(0,0,0,0.1); |
|
|
} |
|
|
.progress-container { |
|
|
margin-top: 20px; |
|
|
display: none; |
|
|
} |
|
|
#uploadInfo { |
|
|
margin-top: 10px; |
|
|
font-size: 0.9em; |
|
|
} |
|
|
#downloadLinkContainer { |
|
|
margin-top: 20px; |
|
|
display: none; |
|
|
} |
|
|
.loader { |
|
|
border: 5px solid #f3f3f3; |
|
|
border-top: 5px solid #3498db; |
|
|
border-radius: 50%; |
|
|
width: 30px; |
|
|
height: 30px; |
|
|
animation: spin 1s linear infinite; |
|
|
display: none; |
|
|
margin: 10px auto; |
|
|
} |
|
|
@keyframes spin { |
|
|
0% { transform: rotate(0deg); } |
|
|
100% { transform: rotate(360deg); } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="container"> |
|
|
<div class="upload-form"> |
|
|
<h2 class="text-center mb-4">Upload a File</h2> |
|
|
<form id="uploadForm" enctype="multipart/form-data"> |
|
|
<div class="mb-3"> |
|
|
<label for="fileInput" class="form-label">Choose file</label> |
|
|
<input class="form-control" type="file" id="fileInput" name="file" required> |
|
|
</div> |
|
|
<button type="submit" class="btn btn-primary w-100">Upload</button> |
|
|
</form> |
|
|
|
|
|
<div class="loader" id="loader"></div> |
|
|
|
|
|
<div class="progress-container" id="progressContainer"> |
|
|
<div class="progress"> |
|
|
<div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div> |
|
|
</div> |
|
|
<div id="uploadInfo" class="text-muted"></div> |
|
|
</div> |
|
|
|
|
|
<div id="downloadLinkContainer" class="alert alert-success" role="alert"> |
|
|
<strong>Success!</strong> File uploaded. <br> |
|
|
Download link: <a href="#" id="downloadLink" target="_blank"></a> |
|
|
</div> |
|
|
|
|
|
<div id="errorContainer" class="alert alert-danger mt-3" role="alert" style="display: none;"> |
|
|
<strong>Error:</strong> <span id="errorMessage"></span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script> |
|
|
<script> |
|
|
const uploadForm = document.getElementById('uploadForm'); |
|
|
const fileInput = document.getElementById('fileInput'); |
|
|
const loader = document.getElementById('loader'); |
|
|
const progressContainer = document.getElementById('progressContainer'); |
|
|
const progressBar = document.getElementById('progressBar'); |
|
|
const uploadInfo = document.getElementById('uploadInfo'); |
|
|
const downloadLinkContainer = document.getElementById('downloadLinkContainer'); |
|
|
const downloadLink = document.getElementById('downloadLink'); |
|
|
const errorContainer = document.getElementById('errorContainer'); |
|
|
const errorMessage = document.getElementById('errorMessage'); |
|
|
|
|
|
let startTime; |
|
|
|
|
|
uploadForm.addEventListener('submit', async function(event) { |
|
|
event.preventDefault(); |
|
|
|
|
|
const file = fileInput.files[0]; |
|
|
if (!file) { |
|
|
alert('Please select a file to upload.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const formData = new FormData(); |
|
|
formData.append('file', file); |
|
|
|
|
|
|
|
|
loader.style.display = 'block'; |
|
|
progressContainer.style.display = 'block'; |
|
|
progressBar.style.width = '0%'; |
|
|
progressBar.textContent = '0%'; |
|
|
uploadInfo.textContent = 'Starting upload...'; |
|
|
downloadLinkContainer.style.display = 'none'; |
|
|
errorContainer.style.display = 'none'; |
|
|
|
|
|
startTime = Date.now(); |
|
|
|
|
|
const xhr = new XMLHttpRequest(); |
|
|
xhr.open('POST', '/upload/', true); |
|
|
|
|
|
xhr.upload.onprogress = function(event) { |
|
|
if (event.lengthComputable) { |
|
|
const percentComplete = Math.round((event.loaded / event.total) * 100); |
|
|
progressBar.style.width = percentComplete + '%'; |
|
|
progressBar.textContent = percentComplete + '%'; |
|
|
|
|
|
const elapsedTime = (Date.now() - startTime) / 1000; |
|
|
const speed = event.loaded / elapsedTime; |
|
|
const speedMbps = (speed * 8 / 1000000).toFixed(2); |
|
|
|
|
|
uploadInfo.textContent = `Uploaded ${formatBytes(event.loaded)} of ${formatBytes(event.total)} (${percentComplete}%) at ${speedMbps} Mbps`; |
|
|
} |
|
|
}; |
|
|
|
|
|
xhr.onload = function() { |
|
|
loader.style.display = 'none'; |
|
|
if (xhr.status === 200) { |
|
|
const response = JSON.parse(xhr.responseText); |
|
|
uploadInfo.textContent = 'Upload complete!'; |
|
|
progressBar.classList.remove('progress-bar-animated'); |
|
|
progressBar.classList.add('bg-success'); |
|
|
downloadLink.href = response.download_url; |
|
|
|
|
|
downloadLink.textContent = response.filename; |
|
|
downloadLinkContainer.style.display = 'block'; |
|
|
} else { |
|
|
progressBar.classList.remove('progress-bar-animated'); |
|
|
progressBar.classList.add('bg-danger'); |
|
|
try { |
|
|
const errorResponse = JSON.parse(xhr.responseText); |
|
|
errorMessage.textContent = errorResponse.detail || `Server error: ${xhr.status}`; |
|
|
} catch (e) { |
|
|
errorMessage.textContent = `Server error: ${xhr.status} - ${xhr.statusText}`; |
|
|
} |
|
|
errorContainer.style.display = 'block'; |
|
|
uploadInfo.textContent = 'Upload failed.'; |
|
|
} |
|
|
}; |
|
|
|
|
|
xhr.onerror = function() { |
|
|
loader.style.display = 'none'; |
|
|
progressBar.classList.remove('progress-bar-animated'); |
|
|
progressBar.classList.add('bg-danger'); |
|
|
errorMessage.textContent = 'Network error or server unavailable.'; |
|
|
errorContainer.style.display = 'block'; |
|
|
uploadInfo.textContent = 'Upload failed.'; |
|
|
}; |
|
|
|
|
|
xhr.send(formData); |
|
|
}); |
|
|
|
|
|
function formatBytes(bytes, decimals = 2) { |
|
|
if (bytes === 0) return '0 Bytes'; |
|
|
const k = 1024; |
|
|
const dm = decimals < 0 ? 0 : decimals; |
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; |
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; |
|
|
} |
|
|
</script> |
|
|
</body> |
|
|
</html> |