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', '=$var['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;