fix #4598 - user experience improvements for file upload

This commit is contained in:
sbwalker 2024-09-12 14:04:35 -04:00
parent 044cee30a5
commit 69bc06685f
3 changed files with 49 additions and 31 deletions

View File

@ -387,7 +387,7 @@
var size = Int64.Parse(uploads[upload].Split(':')[1]); // bytes var size = Int64.Parse(uploads[upload].Split(':')[1]); // bytes
var megabits = (size / 1048576.0) * 8; // binary conversion var megabits = (size / 1048576.0) * 8; // binary conversion
var uploadspeed = 2; // 2 Mbps (3G ranges from 300Kbps to 3Mbps) var uploadspeed = (PageState.Alias.Name.Contains("localhost")) ? 100 : 3; // 3 Mbps is FCC minimum for broadband upload
var uploadtime = (megabits / uploadspeed); // seconds var uploadtime = (megabits / uploadspeed); // seconds
var maxattempts = 5; // polling (minimum timeout duration will be 5 seconds) var maxattempts = 5; // polling (minimum timeout duration will be 5 seconds)
var sleep = (int)Math.Ceiling(uploadtime / maxattempts) * 1000; // milliseconds var sleep = (int)Math.Ceiling(uploadtime / maxattempts) * 1000; // milliseconds

View File

@ -425,11 +425,11 @@ namespace Oqtane.Controllers
// POST api/<controller>/upload // POST api/<controller>/upload
[EnableCors(Constants.MauiCorsPolicy)] [EnableCors(Constants.MauiCorsPolicy)]
[HttpPost("upload")] [HttpPost("upload")]
public async Task UploadFile(string folder, IFormFile formfile) public async Task<IActionResult> UploadFile(string folder, IFormFile formfile)
{ {
if (formfile == null || formfile.Length <= 0) if (formfile == null || formfile.Length <= 0)
{ {
return; return NoContent();
} }
// ensure filename is valid // ensure filename is valid
@ -437,7 +437,7 @@ namespace Oqtane.Controllers
if (!formfile.FileName.IsPathOrFileValid() || !formfile.FileName.Contains(token) || !HasValidFileExtension(formfile.FileName.Substring(0, formfile.FileName.IndexOf(token)))) if (!formfile.FileName.IsPathOrFileValid() || !formfile.FileName.Contains(token) || !HasValidFileExtension(formfile.FileName.Substring(0, formfile.FileName.IndexOf(token))))
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName); _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName);
return; return NoContent();
} }
string folderPath = ""; string folderPath = "";
@ -492,6 +492,8 @@ namespace Oqtane.Controllers
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Upload Attempt {Folder} {File}", folder, formfile.FileName); _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Upload Attempt {Folder} {File}", folder, formfile.FileName);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
} }
return NoContent();
} }
private async Task<string> MergeFile(string folder, string filename) private async Task<string> MergeFile(string folder, string filename)

View File

@ -293,41 +293,49 @@ Oqtane.Interop = {
}, },
uploadFiles: function (posturl, folder, id, antiforgerytoken, jwt) { uploadFiles: function (posturl, folder, id, antiforgerytoken, jwt) {
var fileinput = document.getElementById('FileInput_' + id); var fileinput = document.getElementById('FileInput_' + id);
var files = fileinput.files;
var progressinfo = document.getElementById('ProgressInfo_' + id); var progressinfo = document.getElementById('ProgressInfo_' + id);
var progressbar = document.getElementById('ProgressBar_' + id); var progressbar = document.getElementById('ProgressBar_' + id);
if (progressinfo !== null && progressbar !== null) { if (progressinfo !== null && progressbar !== null) {
progressinfo.setAttribute("style", "display: inline;"); progressinfo.setAttribute("style", "display: inline;");
progressinfo.innerHTML = '';
progressbar.setAttribute("style", "width: 100%; display: inline;"); progressbar.setAttribute("style", "width: 100%; display: inline;");
progressbar.value = 0;
} }
var files = fileinput.files;
var totalSize = 0;
for (var i = 0; i < files.length; i++) {
totalSize = totalSize + files[i].size;
}
var maxChunkSizeMB = 1;
var bufferChunkSize = maxChunkSizeMB * (1024 * 1024);
var uploadedSize = 0;
for (var i = 0; i < files.length; i++) { for (var i = 0; i < files.length; i++) {
var FileChunk = []; var fileChunk = [];
var file = files[i]; var file = files[i];
var MaxFileSizeMB = 1; var fileStreamPos = 0;
var BufferChunkSize = MaxFileSizeMB * (1024 * 1024); var endPos = bufferChunkSize;
var FileStreamPos = 0;
var EndPos = BufferChunkSize;
var Size = file.size;
while (FileStreamPos < Size) { while (fileStreamPos < file.size) {
FileChunk.push(file.slice(FileStreamPos, EndPos)); fileChunk.push(file.slice(fileStreamPos, endPos));
FileStreamPos = EndPos; fileStreamPos = endPos;
EndPos = FileStreamPos + BufferChunkSize; endPos = fileStreamPos + bufferChunkSize;
} }
var TotalParts = FileChunk.length; var totalParts = fileChunk.length;
var PartCount = 0; var partCount = 0;
while (Chunk = FileChunk.shift()) { while (chunk = fileChunk.shift()) {
PartCount++; partCount++;
var FileName = file.name + ".part_" + PartCount.toString().padStart(3, '0') + "_" + TotalParts.toString().padStart(3, '0'); var fileName = file.name + ".part_" + partCount.toString().padStart(3, '0') + "_" + totalParts.toString().padStart(3, '0');
var data = new FormData(); var data = new FormData();
data.append('__RequestVerificationToken', antiforgerytoken); data.append('__RequestVerificationToken', antiforgerytoken);
data.append('folder', folder); data.append('folder', folder);
data.append('formfile', Chunk, FileName); data.append('formfile', chunk, fileName);
var request = new XMLHttpRequest(); var request = new XMLHttpRequest();
request.open('POST', posturl, true); request.open('POST', posturl, true);
if (jwt !== "") { if (jwt !== "") {
@ -335,28 +343,36 @@ Oqtane.Interop = {
request.withCredentials = true; request.withCredentials = true;
} }
request.upload.onloadstart = function (e) { request.upload.onloadstart = function (e) {
if (progressinfo !== null && progressbar !== null) { if (progressinfo !== null && progressbar !== null && progressinfo.innerHTML === '') {
progressinfo.innerHTML = file.name + ' 0%'; if (files.length === 1) {
progressbar.value = 0; progressinfo.innerHTML = file.name;
}
else {
progressinfo.innerHTML = file.name + ", ...";
}
} }
}; };
request.upload.onprogress = function (e) { request.upload.onprogress = function (e) {
if (progressinfo !== null && progressbar !== null) { if (progressinfo !== null && progressbar !== null) {
var percent = Math.ceil((e.loaded / e.total) * 100); var percent = Math.ceil(((uploadedSize + e.loaded) / totalSize) * 100);
progressinfo.innerHTML = file.name + '[' + PartCount + '] ' + percent + '%';
progressbar.value = (percent / 100); progressbar.value = (percent / 100);
} }
}; };
request.upload.onloadend = function (e) { request.upload.onloadend = function (e) {
if (progressinfo !== null && progressbar !== null) { if (progressinfo !== null && progressbar !== null) {
progressinfo.innerHTML = file.name + ' 100%'; uploadedSize = uploadedSize + e.total;
progressbar.value = 1; var percent = Math.ceil((uploadedSize / totalSize) * 100);
progressbar.value = (percent / 100);
} }
}; };
request.upload.onerror = function() { request.upload.onerror = function() {
if (progressinfo !== null && progressbar !== null) { if (progressinfo !== null && progressbar !== null) {
if (files.length === 1) {
progressinfo.innerHTML = file.name + ' Error: ' + request.statusText; progressinfo.innerHTML = file.name + ' Error: ' + request.statusText;
progressbar.value = 0; }
else {
progressinfo.innerHTML = ' Error: ' + request.statusText;
}
} }
}; };
request.send(data); request.send(data);