@@ -1247,6 +1247,7 @@ function stopUpload(file,error,errorType) {
12471247 var message = "_(File is removed)_";
12481248 if (errorType === 'timeout') message += "<br><br>_(Upload timed out. Please check your network connection and try again.)_";
12491249 else if (errorType === 'network') message += "<br><br>_(Network error occurred. Please check your connection and try again.)_";
1250+ else if (errorType === 'read') message += "<br><br>_(Failed to read file. The file may have been removed or is inaccessible.)_";
12501251 else if (errorType && errorType.indexOf('http') === 0) message += "<br><br>_(HTTP error: )_" + errorType.substring(5);
12511252 setTimeout(function(){swal({title:"_(Upload Error)_",text:message,html:true,confirmButtonText:"_(Ok)_"});},200);
12521253 }
@@ -1262,12 +1263,16 @@ function downloadFile(source) {
12621263 document.body.removeChild(a);
12631264}
12641265
1265- function uploadFile(files,index,start,time) {
1266+ function uploadFile(files,index,start,time,prefetchedBuffer ) {
12661267 var file = files[index];
12671268 var slice = 20971520; // 20MB chunks - no Base64 overhead, raw binary
12681269 var next = start + slice;
12691270 var blob = file.slice(start, next);
1270-
1271+
1272+ // Start prefetching next chunk from disk immediately, in parallel with sending current chunk
1273+ var nextBlob = (next < file.size) ? file.slice(next, next + slice) : null;
1274+ var nextBufferPromise = nextBlob ? nextBlob.arrayBuffer() : null;
1275+
12711276 var xhr = new XMLHttpRequest();
12721277 currentXhr = xhr; // Store for abort capability
12731278 var filePath = dir.replace(/\/+$/, '') + '/' + file.name;
@@ -1276,8 +1281,12 @@ function uploadFile(files,index,start,time) {
12761281 xhr.setRequestHeader('Content-Type', 'application/octet-stream');
12771282 xhr.setRequestHeader('X-CSRF-Token', '<?=$var['csrf_token']?>');
12781283 xhr.timeout = Math.max(600000, slice / 1024 * 60); // ~1 minute per MB, minimum 10 minutes
1279-
1284+
12801285 xhr.onload = function() {
1286+ if (cancel === 1) {
1287+ stopUpload(file.name, false);
1288+ return;
1289+ }
12811290 if (xhr.status < 200 || xhr.status >= 300) {
12821291 stopUpload(file.name, true, 'http:' + xhr.status);
12831292 return;
@@ -1286,7 +1295,7 @@ function uploadFile(files,index,start,time) {
12861295 if (reply == 'stop') {stopUpload(file.name); return;}
12871296 if (reply.indexOf('error') === 0) {
12881297 console.error('Upload error:', reply);
1289- stopUpload(file.name,true);
1298+ stopUpload(file.name,true);
12901299 return;
12911300 }
12921301 if (next < file.size) {
@@ -1302,14 +1311,30 @@ function uploadFile(files,index,start,time) {
13021311 var speed = autoscale(bytesTransferred / elapsedSeconds);
13031312 var percent = Math.floor(bytesTransferred / total * 100);
13041313 $('#dfm_uploadStatus').html("_(Uploading)_: <span class='dfm_percent'>"+percent+"%</span><span class='dfm_speed'>Speed: "+speed+"</span><span class='orange-text'> ["+(index+1)+'/'+files.length+'] '+escapeHtml(file.name)+"</span>");
1305- uploadFile(files,index,next,time);
1314+ if (cancel === 1) {
1315+ stopUpload(file.name, false);
1316+ return;
1317+ }
1318+ nextBufferPromise.then(function(nextBuffer) {
1319+ if (cancel === 1) {
1320+ stopUpload(file.name, false);
1321+ return;
1322+ }
1323+ uploadFile(files,index,next,time,nextBuffer);
1324+ }).catch(function() {
1325+ if (cancel !== 1) stopUpload(file.name, true, 'read');
1326+ });
13061327 } else if (index < files.length-1) {
1328+ if (cancel === 1) {
1329+ stopUpload(file.name, false);
1330+ return;
1331+ }
13071332 // Clean up temp file for completed upload before starting next file
13081333 $.post('/webGui/include/Control.php',{mode:'stop',file:encodeURIComponent(file.name)});
13091334 uploadFile(files,index+1,0,time);
13101335 } else {stopUpload(file.name); return;}
13111336 };
1312-
1337+
13131338 xhr.onabort = function() {
13141339 // User cancelled upload - trigger deletion via cancel=1 parameter
13151340 $.post('/webGui/include/Control.php', {
@@ -1322,18 +1347,27 @@ function uploadFile(files,index,start,time) {
13221347 stopUpload(file.name, false);
13231348 });
13241349 };
1325-
1350+
13261351 xhr.onerror = function() {
13271352 // Don't show error if it was a user cancel
13281353 if (cancel === 1) return;
13291354 stopUpload(file.name, true, 'network');
13301355 };
1331-
1356+
13321357 xhr.ontimeout = function() {
13331358 stopUpload(file.name, true, 'timeout');
13341359 };
1335-
1336- xhr.send(blob);
1360+
1361+ // On Safari/WebKit, XHR with a file-backed Blob as request body sends successfully but never reaches
1362+ // readyState 4 (DONE), so onload never fires and the upload loop stalls after the first chunk.
1363+ // Converting to ArrayBuffer first uses a different WebKit code path that behaves correctly.
1364+ // To minimize the disk-read overhead, the next chunk is prefetched in parallel while this one sends.
1365+ (prefetchedBuffer ? Promise.resolve(prefetchedBuffer) : blob.arrayBuffer()).then(function(buffer) {
1366+ if (cancel === 1) return;
1367+ xhr.send(buffer);
1368+ }).catch(function() {
1369+ stopUpload(file.name, true, 'read');
1370+ });
13371371}
13381372
13391373var cancel = 0;
0 commit comments