Large file streaming uploads

This commit is contained in:
Shaun Walker 2019-09-06 13:15:18 -04:00
parent 22420f2b43
commit a84eee8782
17 changed files with 305 additions and 13 deletions

4
.gitignore vendored
View File

@ -6,7 +6,9 @@ artifacts/
msbuild.binlog
.vscode/
*.binlog
*.nupkg
Oqtane.Server/appsettings.json
Oqtane.Server/Data/*.mdf
Oqtane.Server/Data/*.ldf
Oqtane.Server/Data/*.ldf

View File

@ -0,0 +1,30 @@
@using Microsoft.AspNetCore.Components.Routing
@using Oqtane.Client.Modules.Controls
@using Oqtane.Modules
@using Oqtane.Services
@inherits ModuleBase
@inject IUriHelper UriHelper
@inject IFileService FileService
<table class="table table-borderless">
<tr>
<td>
<label for="Name" class="control-label">Module: </label>
</td>
<td>
<FileUpload Filter=".nupkg"></FileUpload>
</td>
</tr>
</table>
<button type="button" class="btn btn-success" @onclick="UploadFile">Upload</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">Cancel</NavLink>
@code {
public override SecurityAccessLevel SecurityAccessLevel { get { return SecurityAccessLevel.Host; } }
private async Task UploadFile()
{
await FileService.UploadFilesAsync("/Sites/Modules");
}
}

View File

@ -12,6 +12,7 @@
}
else
{
<ActionLink Action="Add" Text="Upload Module" />
<table class="table table-borderless">
<thead>
<tr>

View File

@ -0,0 +1,43 @@
@if (multiple)
{
<input type="file" id="@fileid" name="file" accept="@filter" multiple />
}
else
{
<input type="file" id="@fileid" name="file" accept="@filter" />
}
<span id="@progressinfoid"></span> <progress id="@progressbarid" style="visibility: hidden;"></progress>
@code {
[Parameter]
public string Name { get; set; } // optional - can be used for managing multiple file upload controls on a page
[Parameter]
public string Filter { get; set; } // optional - for restricting types of files that can be selected
[Parameter]
public string Multiple { get; set; } // optional - enable multiple file uploads
string fileid = "";
string progressinfoid = "";
string progressbarid = "";
string filter = "*";
bool multiple = false;
protected override void OnInitialized()
{
fileid = Name + "FileInput";
progressinfoid = Name + "ProgressInfo";
progressbarid = Name + "ProgressBar";
if (!string.IsNullOrEmpty(Filter))
{
filter = Filter;
}
if (!string.IsNullOrEmpty(Multiple))
{
multiple = bool.Parse(Multiple);
}
}
}

View File

@ -67,9 +67,9 @@
<table class="table">
<tbody>
<tr>
<td><label for="Username" class="control-label">User: </label></td>
<td style="text-align: right;"><label for="Username" class="control-label">User: </label></td>
<td><input type="text" name="Username" class="form-control" placeholder="Enter Username" @bind="@username" /></td>
<td><button type="button" class="btn btn-primary" @onclick="@AddUser">Add</button></td>
<td style="text-align: left;"><button type="button" class="btn btn-primary" @onclick="@AddUser">Add</button></td>
</tr>
</tbody>
</table>

View File

@ -5,6 +5,6 @@
SecurityAccessLevel SecurityAccessLevel { get; } // defines the security access level for this control - defaults to View
string Title { get; } // title to display for this control - defaults to module title
string Actions { get; } // allows for routing by configuration rather than by convention ( comma delimited ) - defaults to using component file name
string ContainerType { get; } // container for embedding control - defaults to AdminContainer
bool UseAdminContainer { get; } // container for embedding module control - defaults to true
}
}

View File

@ -18,7 +18,7 @@ namespace Oqtane.Modules
public virtual string Actions { get { return ""; } }
public virtual string ContainerType { get { return ""; } }
public virtual bool UseAdminContainer { get { return true; } }
public string NavigateUrl()
{

View File

@ -0,0 +1,37 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Oqtane.Shared;
namespace Oqtane.Services
{
public class FileService : ServiceBase, IFileService
{
private readonly SiteState sitestate;
private readonly IUriHelper urihelper;
private readonly IJSRuntime jsRuntime;
public FileService(SiteState sitestate, IUriHelper urihelper, IJSRuntime jsRuntime)
{
this.sitestate = sitestate;
this.urihelper = urihelper;
this.jsRuntime = jsRuntime;
}
private string apiurl
{
get { return CreateApiUrl(sitestate.Alias, urihelper.GetAbsoluteUri(), "File"); }
}
public async Task UploadFilesAsync(string Folder)
{
await UploadFilesAsync(Folder, "");
}
public async Task UploadFilesAsync(string Folder, string FileUploadName)
{
var interop = new Interop(jsRuntime);
await interop.UploadFiles(apiurl + "/upload", Folder, FileUploadName);
}
}
}

View File

@ -0,0 +1,12 @@
using Oqtane.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Oqtane.Services
{
public interface IFileService
{
Task UploadFilesAsync(string Folder);
Task UploadFilesAsync(string Folder, string FileUploadName);
}
}

View File

@ -47,13 +47,9 @@
{
ModuleState = Module; // passed in from Pane component
container = ModuleState.ContainerType;
if (PageState.ModuleId != -1 && PageState.Control != "")
if (PageState.ModuleId != -1 && PageState.Control != "" && ModuleState.UseAdminContainer)
{
container = ModuleState.AdminContainerType;
if (container == "")
{
container = Constants.DefaultAdminContainer;
}
container = Constants.DefaultAdminContainer;
}
}
return Task.CompletedTask;

View File

@ -85,5 +85,20 @@ namespace Oqtane.Shared
return Task.CompletedTask;
}
}
public Task UploadFiles(string posturl, string folder, string name)
{
try
{
jsRuntime.InvokeAsync<string>(
"interop.uploadFiles",
posturl, folder, name);
return Task.CompletedTask;
}
catch
{
return Task.CompletedTask;
}
}
}
}

View File

@ -349,7 +349,7 @@
module.SecurityAccessLevel = (SecurityAccessLevel)moduletype.GetProperty("SecurityAccessLevel").GetValue(moduleobject, null);
module.ControlTitle = (string)moduletype.GetProperty("Title").GetValue(moduleobject);
module.Actions = (string)moduletype.GetProperty("Actions").GetValue(moduleobject);
module.AdminContainerType = (string)moduletype.GetProperty("ContainerType").GetValue(moduleobject);
module.UseAdminContainer = (bool)moduletype.GetProperty("UseAdminContainer").GetValue(moduleobject);
}
}

View File

@ -50,6 +50,7 @@ namespace Oqtane.Client
services.AddScoped<IRoleService, RoleService>();
services.AddScoped<IUserRoleService, UserRoleService>();
services.AddScoped<ISettingService, SettingService>();
services.AddScoped<IFileService, FileService>();
// dynamically register module contexts and repository services
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();

View File

@ -0,0 +1,98 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.IO;
using System.Threading.Tasks;
namespace Oqtane.Controllers
{
[Route("{site}/api/[controller]")]
public class FileController : Controller
{
private readonly IWebHostEnvironment environment;
public FileController(IWebHostEnvironment environment)
{
this.environment = environment;
}
// GET api/<controller>/current
[HttpPost("upload")]
public async Task UploadFile(string folder, IFormFile file)
{
if (file.Length > 0)
{
if (!folder.Contains(":\\"))
{
folder = folder.Replace("/", "\\");
if (folder.StartsWith("\\")) folder = folder.Substring(1);
folder = Path.Combine(environment.WebRootPath, folder);
}
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
using (var stream = new FileStream(Path.Combine(folder, file.FileName), FileMode.Create))
{
await file.CopyToAsync(stream);
}
await MergeFile(folder, file.FileName);
}
}
private async Task MergeFile(string folder, string filename)
{
// parse the filename which is in the format of filename.ext.part_x_y
string token = ".part_";
string parts = Path.GetExtension(filename).Replace(token, ""); // returns "x_y"
int totalparts = int.Parse(parts.Substring(parts.IndexOf("_") + 1));
filename = filename.Substring(0, filename.IndexOf(token)); // base filename
string[] fileparts = Directory.GetFiles(folder, filename + token + "*"); // list of all file parts
// if all of the file parts exist ( note that file parts can arrive out of order )
if (fileparts.Length == totalparts)
{
// merge file parts
bool success = true;
using (var stream = new FileStream(Path.Combine(folder, filename), FileMode.Create))
{
foreach (string filepart in fileparts)
{
try
{
using (FileStream chunk = new FileStream(filepart, FileMode.Open))
{
await chunk.CopyToAsync(stream);
}
}
catch
{
success = false;
}
}
}
// delete file parts
if (success)
{
foreach (string filepart in fileparts)
{
System.IO.File.Delete(filepart);
}
}
}
// clean up file parts which are more than 2 hours old ( which can happen if a file upload failed )
fileparts = Directory.GetFiles(folder, "*" + token + "*");
foreach (string filepart in fileparts)
{
DateTime createddate = System.IO.File.GetCreationTime(filepart);
if (createddate < DateTime.Now.AddHours(-2))
{
System.IO.File.Delete(filepart);
}
}
}
}
}

View File

@ -95,6 +95,7 @@ namespace Oqtane.Server
services.AddScoped<IRoleService, RoleService>();
services.AddScoped<IUserRoleService, UserRoleService>();
services.AddScoped<ISettingService, SettingService>();
services.AddScoped<IFileService, FileService>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

View File

@ -55,5 +55,61 @@ window.interop = {
document.body.appendChild(form);
form.submit();
},
uploadFiles: function (posturl, folder, name) {
var files = document.getElementById(name + 'FileInput').files;
var progressinfo = document.getElementById(name + 'ProgressInfo');
var progressbar = document.getElementById(name + 'ProgressBar');
var filename = '';
for (var i = 0; i < files.length; i++) {
var FileChunk = [];
var file = files[i];
var MaxFileSizeMB = 1;
var BufferChunkSize = MaxFileSizeMB * (1024 * 1024);
var FileStreamPos = 0;
var EndPos = BufferChunkSize;
var Size = file.size;
progressbar.setAttribute("style", "visibility: visible;");
if (files.length > 1) {
filename = file.name;
}
while (FileStreamPos < Size) {
FileChunk.push(file.slice(FileStreamPos, EndPos));
FileStreamPos = EndPos;
EndPos = FileStreamPos + BufferChunkSize;
}
var TotalParts = FileChunk.length;
var PartCount = 0;
while (Chunk = FileChunk.shift()) {
PartCount++;
var FileName = file.name + ".part_" + PartCount + "_" + TotalParts;
var data = new FormData();
data.append('folder', folder);
data.append('file', Chunk, FileName);
var request = new XMLHttpRequest();
request.open('POST', posturl, true);
request.upload.onloadstart = function (e) {
progressbar.value = 0;
progressinfo.innerHTML = filename + ' 0%';
};
request.upload.onprogress = function (e) {
var percent = Math.ceil((e.loaded / e.total) * 100);
progressbar.value = (percent / 100);
progressinfo.innerHTML = filename + '[' + PartCount + '] ' + percent + '%';
};
request.upload.onloadend = function (e) {
progressbar.value = 1;
progressinfo.innerHTML = filename + ' 100%';
};
request.send(data);
}
}
}
};

View File

@ -49,6 +49,6 @@ namespace Oqtane.Models
[NotMapped]
public string Actions { get; set; }
[NotMapped]
public string AdminContainerType { get; set; }
public bool UseAdminContainer { get; set; }
}
}