ns_upload_stats - Return Real-Time Statistics for an Ongoing File Upload
This command provides real-time statistics for large file uploads, enabling user interfaces to display progress information about uploads in progress.
Internally, an upload request is identified by a unique key, which is used to track statistics such as the current uploaded size and the total size. These statistics are dynamically updated throughout the upload process. Once the upload completes, the statistics are automatically discarded.
The upload key can be provided in several ways:
Recommended: Via the HTTP request header field X-Progress-ID (see example below).
Via a query parameter.
Derived from the URL.
To enable upload progress statistics, the configuration parameter progressminsize must be set. This ensures statistics are maintained only for files exceeding a specified minimum size, reducing unnecessary overhead for small files.
The key parameter is typically the value provided via the X-Progress-ID request header, although query parameters or URLs can also be used. The convention of using the X-Progress-ID header is widely supported by other web servers, including Nginx, Lighttpd, and Apache (with mod_passenger).
This command returns a list with two elements:
The number of bytes uploaded so far.
The total size of the file being uploaded.
To minimize unnecessary overhead for small uploads, progress statistics are only maintained for files larger than a configurable size. Set the progressminsize parameter to specify the minimum file size (e.g., 2kB).
ns_section ns/parameters { ns_param progressminsize 2kB }
JavaScript file for uploading a file, supplying for the upload a unique X-Progress-ID.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>File Upload with Progress</title> </head> <body> <h1>File Upload with X-Progress-ID</h1> <form id="uploadForm" enctype="multipart/form-data"> <input type="file" name="file" id="fileInput" /> <button type="button" id="uploadButton">Upload</button> </form> <p id="progressText">Progress: 0%</p> <div id="progressBar" style="width: 0%; height: 20px; background-color: green;"></div> <script> document.getElementById('uploadButton').addEventListener('click', function () { const fileInput = document.getElementById('fileInput'); if (fileInput.files.length === 0) { alert('Please select a file to upload.'); return; } const progressId = generateUUID(); const formData = new FormData(); formData.append('file', fileInput.files[0]); // Start the file upload with X-Progress-ID header const xhr = new XMLHttpRequest(); xhr.open('POST', '/upload', true); xhr.setRequestHeader('X-Progress-ID', progressId); xhr.upload.addEventListener('loadstart', () => { document.getElementById('progressText').innerText = 'Upload started...'; }); xhr.upload.addEventListener('error', () => { document.getElementById('progressText').innerText = 'Error during upload.'; }); xhr.addEventListener('load', () => { document.getElementById('progressText').innerText = 'Upload completed.'; }); xhr.send(formData); // Query upload progress const progressInterval = setInterval(() => { fetch(`/progress?X-Progress-ID=${progressId}`) .then(response => { if (response.ok) { return response.json(); } else if (response.status === 429) { // request block told us to slow down, could be handled differently return {status: "continue"} } else { console.warn(`Non-200 status code: ${response.status}`); return null; } }) .then(data => { //console.log(data); if (data && data.total) { const percent = Math.round((data.current / data.total) * 100); document.getElementById('progressText').innerText = `Progress: ${percent}%`; document.getElementById('progressBar').style.width = `${percent}%`; if (percent === 100) { clearInterval(progressInterval); } } }) .catch(err => { console.error('Error querying progress:', err); clearInterval(progressInterval); }); },250); }); function generateUUID() { // Simple UUID generator for X-Progress-ID return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } </script> </body> </html>
Server-side support:
# Upload handler ns_register_proc POST /upload { ns_return 200 text/plain "Upload successful" } # Progress handler ns_register_proc GET /progress { set progress_id [ns_queryget X-Progress-ID] if {$progress_id ne ""} { set stats [ns_upload_stats $progress_id] if {[llength $stats] == 2} { lassign $stats current total ns_return 200 application/json [subst {{"total":$total,"current":$current}}] } else { ns_return 404 application/json {{"error": "No progress data available."}} } } else { ns_return 400 application/json {{"error": "Missing X-Progress-ID."}} } }