diff --git a/Controllers/ResourceController.cs b/Controllers/ResourceController.cs index d9a3cd6..bad2f3c 100644 --- a/Controllers/ResourceController.cs +++ b/Controllers/ResourceController.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using MeowStash.Services; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -11,8 +12,15 @@ namespace MeowStash.Controllers; [ApiController] public class ResourceController : ControllerBase { - [HttpPost("add")] - public async Task AddFiles([FromForm] IFormFileCollection files) + public ResourceController(FileIOService io) + { + _io = io; + } + + private readonly FileIOService _io; + + [HttpPost("upload")] + public async Task UploadFiles([FromForm] IFormFileCollection files) { foreach (var file in files) { @@ -23,4 +31,12 @@ public class ResourceController : ControllerBase return NoContent(); } + + [HttpPost("upload/fragment/{id}")] + public async Task UploadFileFragment([FromRoute] int uploadId, [FromBody] byte[] dataPacket) + { + using var buffer = new MemoryStream(dataPacket); + await _io.WritePartialFile(uploadId, buffer); + return NoContent(); + } } diff --git a/Pages/Index.razor b/Pages/Index.razor index b1e1bd7..4e2dae9 100644 --- a/Pages/Index.razor +++ b/Pages/Index.razor @@ -7,36 +7,108 @@
-@_umsg + +@if (_uploadData.Any()) +{ +
+ @foreach (var item in _uploadData) + { +
+
+ @item.Key.Name +
+
+ @item.Value.Item1% + @item.Value.Item2 +
+
+
+
+ +
+ } +
+} @code { - private string _umsg = "", _umsg_class = "text-hidden"; + + private string GetUploadProgressClassesFor(float percent) + { + return percent switch + { + (< 5f) => "upload-progress-low", + (> 5f) and (< 50f) => "upload-progress-submedial", + (> 50f) and (< 95f) => "upload-progress-supermedial", + (> 95f) and (< 100f) => "upload-progress-nearly-done", + 100f or (> 100f) => "upload-progress-done", + _ => "upload-progress-invalid" + }; + } + + private System.Collections.Concurrent.ConcurrentDictionary _uploadData = new(); + private CancellationTokenSource _cancel = null!; + private async Task SendFiles(InputFileChangeEventArgs e) { - Console.WriteLine($"Files: {e.FileCount}"); + if (_cancel != null) + { + _cancel.Cancel(); + _cancel.Dispose(); + } + _cancel = new(); + var files = e.GetMultipleFiles(e.FileCount); - _umsg_class = "text-green"; + _uploadData = new(files.Count, files.ToDictionary(f => f, _ => (0f, 0L)), null); - int counter = 1; - foreach (var file in files) + if (!System.IO.Directory.Exists("/tmp/uploads")) { - _umsg = $"Uploading {counter}/{e.FileCount} files..."; - StateHasChanged(); - Console.WriteLine($"Fos: /tmp/uploads/{file.Name}"); - if (!System.IO.Directory.Exists("/tmp/uploads")) - { - System.IO.Directory.CreateDirectory("/tmp/uploads"); - } - Console.WriteLine($"fosse"); - await using var fos = System.IO.File.OpenWrite($"/tmp/uploads/{file.Name}"); - Console.WriteLine("Fos opened"); - await file.OpenReadStream(1024 * 1024 * 10).CopyToAsync(fos); - Console.WriteLine($"File: /tmp/uploads/{file.Name} written..."); - ++counter; + System.IO.Directory.CreateDirectory("/tmp/uploads"); } - _umsg = "Done!"; + var tasks = new List(); + + foreach (var file in files) + { + tasks.Add(Task.Run(async () => + { + await using var fos = System.IO.File.OpenWrite($"/tmp/uploads/{file.Name}"); + + var stream = file.OpenReadStream(1024 * 1024 * 1024, _cancel.Token); + int read; + + byte[] buffer = new byte[16 * 1024]; + + while ((read = await stream.ReadAsync(buffer, 0, buffer.Length, _cancel.Token)) > 0) + { + if (_cancel.IsCancellationRequested) + { + stream.Close(); + return; + } + + await fos.WriteAsync(buffer, 0, read); + + long totalRead = _uploadData[file].Item2; + totalRead += read; + + float percent = float.Round(((float)totalRead / (float)file.Size * 100f), 2); + _uploadData[file] = (percent, totalRead); + + await InvokeAsync(StateHasChanged); + await Task.Delay(500, _cancel.Token); + } + + stream.Close(); + + Console.WriteLine($"File: /tmp/uploads/{file.Name} written..."); + await InvokeAsync(StateHasChanged); + })); + } + + await Task.WhenAll(tasks); + StateHasChanged(); } } diff --git a/Program.cs b/Program.cs index c7f71a3..f4d6572 100644 --- a/Program.cs +++ b/Program.cs @@ -1,19 +1,13 @@ -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Web; - var builder = WebApplication.CreateBuilder(args); -// Add services to the container. builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); var app = builder.Build(); -// Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } diff --git a/Services/FileIOService.cs b/Services/FileIOService.cs new file mode 100644 index 0000000..4c0a223 --- /dev/null +++ b/Services/FileIOService.cs @@ -0,0 +1,23 @@ +namespace MeowStash.Services; + +public class FileIOService +{ + public async Task WriteFullFile(string location, byte[] data) + { + await File.WriteAllBytesAsync(location, data); + } + + public async Task WriteFullFile(string location, Stream data) + { + using var stream = File.OpenWrite(location); + await data.CopyToAsync(stream); + stream.Close(); + } + + public async Task WritePartialFile(int id, Stream data) + { + using var stream = File.Open($"/tmp/{id}", FileMode.Append); + await data.CopyToAsync(stream); + stream.Close(); + } +} diff --git a/wwwroot/css/site.css b/wwwroot/css/site.css index f52507a..8e07dcb 100644 --- a/wwwroot/css/site.css +++ b/wwwroot/css/site.css @@ -86,3 +86,84 @@ a, .blazor-error-boundary::after { content: "An error has occurred."; } + +.upload-item { + display: block; +} + +.upload-item-name { + color: lightgray; +} + +.upload-progress { + display: block; + width: 100%; +} + +.upload-progress-value { + display: block; +} + +.upload-progress-bytes { + color: lightgray; + display: block; +} + +.upload-progress-bar-container { + width: 80%; + height: 30px; + background-color: gray; + border-radius: 10px; +} + +span.upload-progress-low { + color: lightcoral; +} + +span.upload-progress-submedial { + color: yellow; +} + +span.upload-progress-supermedial { + color: greenyellow; +} + +span.upload-progress-nearly-done { + color: limegreen; +} + +span.upload-progress-done { + color: green; +} + +span.upload-progress-invalid { + color: darkgray; +} + +.upload-progress-bar { + height: 100%; +} + +.upload-progress-bar.upload-progress-low { + background-color: lightcoral; +} + +.upload-progress-bar.upload-progress-submedial { + background-color: yellow; +} + +.upload-progress-bar.upload-progress-supermedial { + background-color: greenyellow; +} + +.upload-progress-bar.upload-progress-nearly-done { + background-color: limegreen; +} + +.upload-progress-bar.upload-progress-done { + background-color: green; +} + +.upload-progress-bar.upload-progress-invalid { + background-color: darkgray; +}