diff --git a/emhttp/plugins/dynamix/Browse.page b/emhttp/plugins/dynamix/Browse.page index f52893e8b6..d08a06f9db 100644 --- a/emhttp/plugins/dynamix/Browse.page +++ b/emhttp/plugins/dynamix/Browse.page @@ -1247,6 +1247,7 @@ function stopUpload(file,error,errorType) { var message = "_(File is removed)_"; if (errorType === 'timeout') message += "

_(Upload timed out. Please check your network connection and try again.)_"; else if (errorType === 'network') message += "

_(Network error occurred. Please check your connection and try again.)_"; + else if (errorType === 'read') message += "

_(Failed to read file. The file may have been removed or is inaccessible.)_"; else if (errorType && errorType.indexOf('http') === 0) message += "

_(HTTP error: )_" + errorType.substring(5); setTimeout(function(){swal({title:"_(Upload Error)_",text:message,html:true,confirmButtonText:"_(Ok)_"});},200); } @@ -1262,12 +1263,16 @@ function downloadFile(source) { document.body.removeChild(a); } -function uploadFile(files,index,start,time) { +function uploadFile(files,index,start,time,prefetchedBuffer) { var file = files[index]; var slice = 20971520; // 20MB chunks - no Base64 overhead, raw binary var next = start + slice; var blob = file.slice(start, next); - + + // Start prefetching next chunk from disk immediately, in parallel with sending current chunk + var nextBlob = (next < file.size) ? file.slice(next, next + slice) : null; + var nextBufferPromise = nextBlob ? nextBlob.arrayBuffer() : null; + var xhr = new XMLHttpRequest(); currentXhr = xhr; // Store for abort capability var filePath = dir.replace(/\/+$/, '') + '/' + file.name; @@ -1276,8 +1281,12 @@ function uploadFile(files,index,start,time) { xhr.setRequestHeader('Content-Type', 'application/octet-stream'); xhr.setRequestHeader('X-CSRF-Token', ''); xhr.timeout = Math.max(600000, slice / 1024 * 60); // ~1 minute per MB, minimum 10 minutes - + xhr.onload = function() { + if (cancel === 1) { + stopUpload(file.name, false); + return; + } if (xhr.status < 200 || xhr.status >= 300) { stopUpload(file.name, true, 'http:' + xhr.status); return; @@ -1286,7 +1295,7 @@ function uploadFile(files,index,start,time) { if (reply == 'stop') {stopUpload(file.name); return;} if (reply.indexOf('error') === 0) { console.error('Upload error:', reply); - stopUpload(file.name,true); + stopUpload(file.name,true); return; } if (next < file.size) { @@ -1302,14 +1311,30 @@ function uploadFile(files,index,start,time) { var speed = autoscale(bytesTransferred / elapsedSeconds); var percent = Math.floor(bytesTransferred / total * 100); $('#dfm_uploadStatus').html("_(Uploading)_: "+percent+"%Speed: "+speed+" ["+(index+1)+'/'+files.length+']  '+escapeHtml(file.name)+""); - uploadFile(files,index,next,time); + if (cancel === 1) { + stopUpload(file.name, false); + return; + } + nextBufferPromise.then(function(nextBuffer) { + if (cancel === 1) { + stopUpload(file.name, false); + return; + } + uploadFile(files,index,next,time,nextBuffer); + }).catch(function() { + if (cancel !== 1) stopUpload(file.name, true, 'read'); + }); } else if (index < files.length-1) { + if (cancel === 1) { + stopUpload(file.name, false); + return; + } // Clean up temp file for completed upload before starting next file $.post('/webGui/include/Control.php',{mode:'stop',file:encodeURIComponent(file.name)}); uploadFile(files,index+1,0,time); } else {stopUpload(file.name); return;} }; - + xhr.onabort = function() { // User cancelled upload - trigger deletion via cancel=1 parameter $.post('/webGui/include/Control.php', { @@ -1322,18 +1347,27 @@ function uploadFile(files,index,start,time) { stopUpload(file.name, false); }); }; - + xhr.onerror = function() { // Don't show error if it was a user cancel if (cancel === 1) return; stopUpload(file.name, true, 'network'); }; - + xhr.ontimeout = function() { stopUpload(file.name, true, 'timeout'); }; - - xhr.send(blob); + + // On Safari/WebKit, XHR with a file-backed Blob as request body sends successfully but never reaches + // readyState 4 (DONE), so onload never fires and the upload loop stalls after the first chunk. + // Converting to ArrayBuffer first uses a different WebKit code path that behaves correctly. + // To minimize the disk-read overhead, the next chunk is prefetched in parallel while this one sends. + (prefetchedBuffer ? Promise.resolve(prefetchedBuffer) : blob.arrayBuffer()).then(function(buffer) { + if (cancel === 1) return; + xhr.send(buffer); + }).catch(function() { + stopUpload(file.name, true, 'read'); + }); } var cancel = 0;