Large file streaming uploads
This commit is contained in:
parent
22420f2b43
commit
a84eee8782
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -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
|
||||
|
||||
|
|
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
|
||||
{
|
||||
<ActionLink Action="Add" Text="Upload Module" />
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<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">
|
||||
<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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
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,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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
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<IUserRoleService, UserRoleService>();
|
||||
services.AddScoped<ISettingService, SettingService>();
|
||||
services.AddScoped<IFileService, FileService>();
|
||||
|
||||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -49,6 +49,6 @@ namespace Oqtane.Models
|
|||
[NotMapped]
|
||||
public string Actions { get; set; }
|
||||
[NotMapped]
|
||||
public string AdminContainerType { get; set; }
|
||||
public bool UseAdminContainer { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user