Large file streaming uploads
This commit is contained in:
parent
22420f2b43
commit
a84eee8782
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -6,7 +6,9 @@ artifacts/
|
||||||
msbuild.binlog
|
msbuild.binlog
|
||||||
.vscode/
|
.vscode/
|
||||||
*.binlog
|
*.binlog
|
||||||
|
*.nupkg
|
||||||
|
|
||||||
Oqtane.Server/appsettings.json
|
Oqtane.Server/appsettings.json
|
||||||
Oqtane.Server/Data/*.mdf
|
Oqtane.Server/Data/*.mdf
|
||||||
Oqtane.Server/Data/*.ldf
|
Oqtane.Server/Data/*.ldf
|
||||||
|
|
||||||
|
|
30
Oqtane.Client/Modules/Admin/ModuleDefinitions/Add.razor
Normal file
30
Oqtane.Client/Modules/Admin/ModuleDefinitions/Add.razor
Normal 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -12,6 +12,7 @@
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<ActionLink Action="Add" Text="Upload Module" />
|
||||||
<table class="table table-borderless">
|
<table class="table table-borderless">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
43
Oqtane.Client/Modules/Controls/FileUpload.razor
Normal file
43
Oqtane.Client/Modules/Controls/FileUpload.razor
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -67,9 +67,9 @@
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<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><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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -5,6 +5,6 @@
|
||||||
SecurityAccessLevel SecurityAccessLevel { get; } // defines the security access level for this control - defaults to View
|
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 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 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ namespace Oqtane.Modules
|
||||||
|
|
||||||
public virtual string Actions { get { return ""; } }
|
public virtual string Actions { get { return ""; } }
|
||||||
|
|
||||||
public virtual string ContainerType { get { return ""; } }
|
public virtual bool UseAdminContainer { get { return true; } }
|
||||||
|
|
||||||
public string NavigateUrl()
|
public string NavigateUrl()
|
||||||
{
|
{
|
||||||
|
|
37
Oqtane.Client/Services/FileService.cs
Normal file
37
Oqtane.Client/Services/FileService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
Oqtane.Client/Services/Interfaces/IFileService.cs
Normal file
12
Oqtane.Client/Services/Interfaces/IFileService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,15 +47,11 @@
|
||||||
{
|
{
|
||||||
ModuleState = Module; // passed in from Pane component
|
ModuleState = Module; // passed in from Pane component
|
||||||
container = ModuleState.ContainerType;
|
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;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,5 +85,20 @@ namespace Oqtane.Shared
|
||||||
return Task.CompletedTask;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -349,7 +349,7 @@
|
||||||
module.SecurityAccessLevel = (SecurityAccessLevel)moduletype.GetProperty("SecurityAccessLevel").GetValue(moduleobject, null);
|
module.SecurityAccessLevel = (SecurityAccessLevel)moduletype.GetProperty("SecurityAccessLevel").GetValue(moduleobject, null);
|
||||||
module.ControlTitle = (string)moduletype.GetProperty("Title").GetValue(moduleobject);
|
module.ControlTitle = (string)moduletype.GetProperty("Title").GetValue(moduleobject);
|
||||||
module.Actions = (string)moduletype.GetProperty("Actions").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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@ namespace Oqtane.Client
|
||||||
services.AddScoped<IRoleService, RoleService>();
|
services.AddScoped<IRoleService, RoleService>();
|
||||||
services.AddScoped<IUserRoleService, UserRoleService>();
|
services.AddScoped<IUserRoleService, UserRoleService>();
|
||||||
services.AddScoped<ISettingService, SettingService>();
|
services.AddScoped<ISettingService, SettingService>();
|
||||||
|
services.AddScoped<IFileService, FileService>();
|
||||||
|
|
||||||
// dynamically register module contexts and repository services
|
// dynamically register module contexts and repository services
|
||||||
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||||
|
|
98
Oqtane.Server/Controllers/FileController.cs
Normal file
98
Oqtane.Server/Controllers/FileController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -95,6 +95,7 @@ namespace Oqtane.Server
|
||||||
services.AddScoped<IRoleService, RoleService>();
|
services.AddScoped<IRoleService, RoleService>();
|
||||||
services.AddScoped<IUserRoleService, UserRoleService>();
|
services.AddScoped<IUserRoleService, UserRoleService>();
|
||||||
services.AddScoped<ISettingService, SettingService>();
|
services.AddScoped<ISettingService, SettingService>();
|
||||||
|
services.AddScoped<IFileService, FileService>();
|
||||||
|
|
||||||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
|
|
||||||
|
|
|
@ -55,5 +55,61 @@ window.interop = {
|
||||||
|
|
||||||
document.body.appendChild(form);
|
document.body.appendChild(form);
|
||||||
form.submit();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -49,6 +49,6 @@ namespace Oqtane.Models
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public string Actions { get; set; }
|
public string Actions { get; set; }
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public string AdminContainerType { get; set; }
|
public bool UseAdminContainer { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user