commit
99022b76e5
|
@ -157,6 +157,9 @@
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false
|
public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<int> OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded
|
public EventCallback<int> OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded
|
||||||
|
|
||||||
|
@ -383,51 +386,8 @@
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt);
|
// upload files
|
||||||
|
var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, ChunkSize);
|
||||||
// uploading is asynchronous so we need to poll to determine if uploads are completed
|
|
||||||
var success = true;
|
|
||||||
int upload = 0;
|
|
||||||
while (upload < uploads.Length && success)
|
|
||||||
{
|
|
||||||
success = false;
|
|
||||||
var filename = uploads[upload].Split(':')[0];
|
|
||||||
|
|
||||||
var size = Int64.Parse(uploads[upload].Split(':')[1]); // bytes
|
|
||||||
var megabits = (size / 1048576.0) * 8; // binary conversion
|
|
||||||
var uploadspeed = (PageState.Alias.Name.Contains("localhost")) ? 100 : 3; // 3 Mbps is FCC minimum for broadband upload
|
|
||||||
var uploadtime = (megabits / uploadspeed); // seconds
|
|
||||||
var maxattempts = 5; // polling (minimum timeout duration will be 5 seconds)
|
|
||||||
var sleep = (int)Math.Ceiling(uploadtime / maxattempts) * 1000; // milliseconds
|
|
||||||
|
|
||||||
int attempts = 0;
|
|
||||||
while (attempts < maxattempts && !success)
|
|
||||||
{
|
|
||||||
attempts += 1;
|
|
||||||
Thread.Sleep(sleep);
|
|
||||||
|
|
||||||
if (Folder == Constants.PackagesFolder)
|
|
||||||
{
|
|
||||||
var files = await FileService.GetFilesAsync(folder);
|
|
||||||
if (files != null && files.Any(item => item.Name == filename))
|
|
||||||
{
|
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var file = await FileService.GetFileAsync(int.Parse(folder), filename);
|
|
||||||
if (file != null)
|
|
||||||
{
|
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (success)
|
|
||||||
{
|
|
||||||
upload++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset progress indicators
|
// reset progress indicators
|
||||||
if (ShowProgress)
|
if (ShowProgress)
|
||||||
|
|
|
@ -209,17 +209,22 @@ namespace Oqtane.UI
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt)
|
public Task UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt)
|
||||||
|
{
|
||||||
|
UploadFiles(posturl, folder, id, antiforgerytoken, jwt, 1);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<bool> UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt, int chunksize)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_jsRuntime.InvokeVoidAsync(
|
return _jsRuntime.InvokeAsync<bool>(
|
||||||
"Oqtane.Interop.uploadFiles",
|
"Oqtane.Interop.uploadFiles",
|
||||||
posturl, folder, id, antiforgerytoken, jwt);
|
posturl, folder, id, antiforgerytoken, jwt, chunksize);
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return new ValueTask<bool>(Task.FromResult(false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ using System.Net.Http;
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using Oqtane.Services;
|
using Oqtane.Services;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
// ReSharper disable StringIndexOfIsCultureSpecific.1
|
// ReSharper disable StringIndexOfIsCultureSpecific.1
|
||||||
|
|
||||||
|
@ -427,7 +428,7 @@ namespace Oqtane.Controllers
|
||||||
// POST api/<controller>/upload
|
// POST api/<controller>/upload
|
||||||
[EnableCors(Constants.MauiCorsPolicy)]
|
[EnableCors(Constants.MauiCorsPolicy)]
|
||||||
[HttpPost("upload")]
|
[HttpPost("upload")]
|
||||||
public async Task<IActionResult> UploadFile(string folder, IFormFile formfile)
|
public async Task<IActionResult> UploadFile([FromForm] string folder, IFormFile formfile)
|
||||||
{
|
{
|
||||||
if (formfile == null || formfile.Length <= 0)
|
if (formfile == null || formfile.Length <= 0)
|
||||||
{
|
{
|
||||||
|
@ -435,13 +436,20 @@ namespace Oqtane.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure filename is valid
|
// ensure filename is valid
|
||||||
string token = ".part_";
|
if (!formfile.FileName.IsPathOrFileValid() || !HasValidFileExtension(formfile.FileName))
|
||||||
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 NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensure headers exist
|
||||||
|
if (!Request.Headers.TryGetValue("PartCount", out StringValues partCount) || !Request.Headers.TryGetValue("TotalParts", out StringValues totalParts))
|
||||||
|
{
|
||||||
|
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload Request Is Missing Required Headers");
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
string fileName = formfile.FileName + ".part_" + int.Parse(partCount).ToString("000") + "_" + int.Parse(totalParts).ToString("000");
|
||||||
string folderPath = "";
|
string folderPath = "";
|
||||||
|
|
||||||
int FolderId;
|
int FolderId;
|
||||||
|
@ -465,12 +473,12 @@ namespace Oqtane.Controllers
|
||||||
if (!string.IsNullOrEmpty(folderPath))
|
if (!string.IsNullOrEmpty(folderPath))
|
||||||
{
|
{
|
||||||
CreateDirectory(folderPath);
|
CreateDirectory(folderPath);
|
||||||
using (var stream = new FileStream(Path.Combine(folderPath, formfile.FileName), FileMode.Create))
|
using (var stream = new FileStream(Path.Combine(folderPath, fileName), FileMode.Create))
|
||||||
{
|
{
|
||||||
await formfile.CopyToAsync(stream);
|
await formfile.CopyToAsync(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
string upload = await MergeFile(folderPath, formfile.FileName);
|
string upload = await MergeFile(folderPath, fileName);
|
||||||
if (upload != "" && FolderId != -1)
|
if (upload != "" && FolderId != -1)
|
||||||
{
|
{
|
||||||
var file = CreateFile(upload, FolderId, Path.Combine(folderPath, upload));
|
var file = CreateFile(upload, FolderId, Path.Combine(folderPath, upload));
|
||||||
|
|
|
@ -308,97 +308,202 @@ Oqtane.Interop = {
|
||||||
}
|
}
|
||||||
return files;
|
return files;
|
||||||
},
|
},
|
||||||
uploadFiles: function (posturl, folder, id, antiforgerytoken, jwt) {
|
uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize) {
|
||||||
|
var success = true;
|
||||||
var fileinput = document.getElementById('FileInput_' + id);
|
var fileinput = document.getElementById('FileInput_' + id);
|
||||||
var progressinfo = document.getElementById('ProgressInfo_' + id);
|
var progressinfo = document.getElementById('ProgressInfo_' + id);
|
||||||
var progressbar = document.getElementById('ProgressBar_' + id);
|
var progressbar = document.getElementById('ProgressBar_' + id);
|
||||||
|
|
||||||
|
var totalSize = 0;
|
||||||
|
for (var i = 0; i < fileinput.files.length; i++) {
|
||||||
|
totalSize += fileinput.files[i].size;
|
||||||
|
}
|
||||||
|
let uploadSize = 0;
|
||||||
|
|
||||||
|
if (!chunksize) {
|
||||||
|
chunksize = 1; // 1 MB default
|
||||||
|
}
|
||||||
|
|
||||||
if (progressinfo !== null && progressbar !== null) {
|
if (progressinfo !== null && progressbar !== null) {
|
||||||
|
progressinfo.setAttribute('style', 'display: inline;');
|
||||||
|
if (fileinput.files.length > 1) {
|
||||||
|
progressinfo.innerHTML = fileinput.files[0].name + ', ...';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
progressinfo.innerHTML = fileinput.files[0].name;
|
||||||
|
}
|
||||||
|
progressbar.setAttribute('style', 'width: 100%; display: inline;');
|
||||||
|
progressbar.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadFiles = Array.from(fileinput.files).map(file => {
|
||||||
|
const uploadFile = () => {
|
||||||
|
const chunkSize = chunksize * (1024 * 1024);
|
||||||
|
const totalParts = Math.ceil(file.size / chunkSize);
|
||||||
|
let partCount = 0;
|
||||||
|
|
||||||
|
const uploadPart = () => {
|
||||||
|
const start = partCount * chunkSize;
|
||||||
|
const end = Math.min(start + chunkSize, file.size);
|
||||||
|
const chunk = file.slice(start, end);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let formdata = new FormData();
|
||||||
|
formdata.append('__RequestVerificationToken', antiforgerytoken);
|
||||||
|
formdata.append('folder', folder);
|
||||||
|
formdata.append('formfile', chunk, file.name);
|
||||||
|
|
||||||
|
var credentials = 'same-origin';
|
||||||
|
var headers = new Headers();
|
||||||
|
headers.append('PartCount', partCount + 1);
|
||||||
|
headers.append('TotalParts', totalParts);
|
||||||
|
if (jwt !== "") {
|
||||||
|
headers.append('Authorization', 'Bearer ' + jwt);
|
||||||
|
credentials = 'include';
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(posturl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers,
|
||||||
|
credentials: credentials,
|
||||||
|
body: formdata
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
if (progressinfo !== null) {
|
||||||
|
progressinfo.innerHTML = 'Error: ' + response.statusText;
|
||||||
|
}
|
||||||
|
throw new Error('Failed');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
partCount++;
|
||||||
|
if (progressbar !== null) {
|
||||||
|
uploadSize += chunk.size;
|
||||||
|
var percent = Math.ceil((uploadSize / totalSize) * 100);
|
||||||
|
progressbar.value = (percent / 100);
|
||||||
|
}
|
||||||
|
if (partCount < totalParts) {
|
||||||
|
uploadPart().then(resolve).catch(reject);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return uploadPart();
|
||||||
|
};
|
||||||
|
|
||||||
|
return uploadFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(uploadFiles);
|
||||||
|
} catch (error) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileinput.value = '';
|
||||||
|
return success;
|
||||||
|
},
|
||||||
|
uploadFile: function (posturl, folder, id, antiforgerytoken, jwt, chunksize, totalsize, file) {
|
||||||
|
var fileinput = document.getElementById('FileInput_' + id);
|
||||||
|
var progressinfo = document.getElementById('ProgressInfo_' + id);
|
||||||
|
var progressbar = document.getElementById('ProgressBar_' + id);
|
||||||
|
|
||||||
|
if (file === null && fileinput !== null) {
|
||||||
|
file = fileinput.files[0];
|
||||||
|
totalsize = file.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressinfo !== null && progressbar !== null && fileinput.files.length === 1) {
|
||||||
progressinfo.setAttribute("style", "display: inline;");
|
progressinfo.setAttribute("style", "display: inline;");
|
||||||
progressinfo.innerHTML = '';
|
progressinfo.innerHTML = file.name;
|
||||||
progressbar.setAttribute("style", "width: 100%; display: inline;");
|
progressbar.setAttribute("style", "width: 100%; display: inline;");
|
||||||
progressbar.value = 0;
|
progressbar.value = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = fileinput.files;
|
if (!chunksize) {
|
||||||
var totalSize = 0;
|
chunksize = 1; // 1 MB default
|
||||||
for (var i = 0; i < files.length; i++) {
|
|
||||||
totalSize = totalSize + files[i].size;
|
|
||||||
}
|
}
|
||||||
|
const chunkSize = chunksize * (1024 * 1024);
|
||||||
|
let uploadSize = 0;
|
||||||
|
const totalParts = Math.ceil(file.size / chunkSize);
|
||||||
|
let partCount = 0;
|
||||||
|
const maxThreads = 1;
|
||||||
|
let threadCount = 1;
|
||||||
|
|
||||||
var maxChunkSizeMB = 1;
|
const uploadPart = () => {
|
||||||
var bufferChunkSize = maxChunkSizeMB * (1024 * 1024);
|
const start = partCount * chunkSize;
|
||||||
var uploadedSize = 0;
|
const end = Math.min(start + chunkSize, file.size);
|
||||||
|
const chunk = file.slice(start, end);
|
||||||
|
|
||||||
for (var i = 0; i < files.length; i++) {
|
while (threadCount > maxThreads) {
|
||||||
var fileChunk = [];
|
// wait for thread to become available
|
||||||
var file = files[i];
|
|
||||||
var fileStreamPos = 0;
|
|
||||||
var endPos = bufferChunkSize;
|
|
||||||
|
|
||||||
while (fileStreamPos < file.size) {
|
|
||||||
fileChunk.push(file.slice(fileStreamPos, endPos));
|
|
||||||
fileStreamPos = endPos;
|
|
||||||
endPos = fileStreamPos + bufferChunkSize;
|
|
||||||
}
|
}
|
||||||
|
threadCount++;
|
||||||
|
|
||||||
var totalParts = fileChunk.length;
|
return new Promise((resolve, reject) => {
|
||||||
var partCount = 0;
|
let formdata = new FormData();
|
||||||
|
formdata.append('__RequestVerificationToken', antiforgerytoken);
|
||||||
|
formdata.append('folder', folder);
|
||||||
|
formdata.append('formfile', chunk, file.name);
|
||||||
|
|
||||||
while (chunk = fileChunk.shift()) {
|
var credentials = 'same-origin';
|
||||||
partCount++;
|
var headers = new Headers();
|
||||||
var fileName = file.name + ".part_" + partCount.toString().padStart(3, '0') + "_" + totalParts.toString().padStart(3, '0');
|
headers.append('PartCount', partCount + 1);
|
||||||
|
headers.append('TotalParts', totalParts);
|
||||||
var data = new FormData();
|
|
||||||
data.append('__RequestVerificationToken', antiforgerytoken);
|
|
||||||
data.append('folder', folder);
|
|
||||||
data.append('formfile', chunk, fileName);
|
|
||||||
var request = new XMLHttpRequest();
|
|
||||||
request.open('POST', posturl, true);
|
|
||||||
if (jwt !== "") {
|
if (jwt !== "") {
|
||||||
request.setRequestHeader('Authorization', 'Bearer ' + jwt);
|
headers.append('Authorization', 'Bearer ' + jwt);
|
||||||
request.withCredentials = true;
|
credentials = 'include';
|
||||||
}
|
}
|
||||||
request.upload.onloadstart = function (e) {
|
|
||||||
if (progressinfo !== null && progressbar !== null && progressinfo.innerHTML === '') {
|
|
||||||
if (files.length === 1) {
|
|
||||||
progressinfo.innerHTML = file.name;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
progressinfo.innerHTML = file.name + ", ...";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
request.upload.onprogress = function (e) {
|
|
||||||
if (progressinfo !== null && progressbar !== null) {
|
|
||||||
var percent = Math.ceil(((uploadedSize + e.loaded) / totalSize) * 100);
|
|
||||||
progressbar.value = (percent / 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
request.upload.onloadend = function (e) {
|
|
||||||
if (progressinfo !== null && progressbar !== null) {
|
|
||||||
uploadedSize = uploadedSize + e.total;
|
|
||||||
var percent = Math.ceil((uploadedSize / totalSize) * 100);
|
|
||||||
progressbar.value = (percent / 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
request.upload.onerror = function() {
|
|
||||||
if (progressinfo !== null && progressbar !== null) {
|
|
||||||
if (files.length === 1) {
|
|
||||||
progressinfo.innerHTML = file.name + ' Error: ' + request.statusText;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
progressinfo.innerHTML = ' Error: ' + request.statusText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
request.send(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i === files.length - 1) {
|
return fetch(posturl, {
|
||||||
fileinput.value = '';
|
method: 'POST',
|
||||||
}
|
headers: headers,
|
||||||
}
|
credentials: credentials,
|
||||||
|
body: formdata
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
if (progressinfo !== null) {
|
||||||
|
progressinfo.innerHTML = ' Error: ' + response.statusText;
|
||||||
|
}
|
||||||
|
throw new Error('Failed');
|
||||||
|
}
|
||||||
|
if (progressbar !== null) {
|
||||||
|
progressbar.value = 1;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
partCount++;
|
||||||
|
if (progressbar !== null) {
|
||||||
|
uploadSize += chunk.size;
|
||||||
|
var percent = Math.ceil((uploadSize / totalsize) * 100);
|
||||||
|
progressbar.value = (percent / 100);
|
||||||
|
}
|
||||||
|
threadCount--;
|
||||||
|
if (partCount < totalParts) {
|
||||||
|
uploadPart().then(resolve).catch(reject);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return uploadPart();
|
||||||
},
|
},
|
||||||
refreshBrowser: function (verify, wait) {
|
refreshBrowser: function (verify, wait) {
|
||||||
async function attemptReload (verify) {
|
async function attemptReload (verify) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user