Skip to content

Commit 375a4cd

Browse files
authored
Merge pull request #2645 from mgutt/fix-upload-truncation
Fix file upload truncation on Safari
2 parents 4099a54 + 7e18408 commit 375a4cd

1 file changed

Lines changed: 44 additions & 10 deletions

File tree

emhttp/plugins/dynamix/Browse.page

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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+']&nbsp;&nbsp;'+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

13391373
var cancel = 0;

0 commit comments

Comments
 (0)