diff --git a/Moonlight.ApiServer/Http/Controllers/Admin/Sys/FilesController.cs b/Moonlight.ApiServer/Http/Controllers/Admin/Sys/FilesController.cs index 5bd609a1..7d77be8b 100644 --- a/Moonlight.ApiServer/Http/Controllers/Admin/Sys/FilesController.cs +++ b/Moonlight.ApiServer/Http/Controllers/Admin/Sys/FilesController.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Mvc; using MoonCore.Exceptions; using MoonCore.Extended.PermFilter; using MoonCore.Helpers; -using Moonlight.ApiServer.Configuration; using Moonlight.Shared.Http.Requests.Admin.Sys.Files; using Moonlight.Shared.Http.Responses.Admin.Sys; @@ -18,6 +17,7 @@ namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys; public class FilesController : Controller { private readonly string BaseDirectory = PathBuilder.Dir("storage"); + private readonly long ChunkSize = ByteConverter.FromMegaBytes(20).Bytes; [HttpGet("list")] public Task List([FromQuery] string path) @@ -64,14 +64,24 @@ public class FilesController : Controller ); } - [HttpPost("create")] - public async Task Create([FromQuery] string path) + [HttpPost("upload")] + public async Task Upload([FromQuery] string path, [FromQuery] long totalSize, [FromQuery] int chunkId) { if (Request.Form.Files.Count != 1) throw new HttpApiException("You need to provide exactly one file", 400); var file = Request.Form.Files[0]; - var stream = file.OpenReadStream(); + + if (file.Length > ChunkSize) + throw new HttpApiException("The provided data exceeds the chunk size limit", 400); + + var chunks = totalSize / ChunkSize; + chunks += totalSize % ChunkSize > 0 ? 1 : 0; + + if (chunkId > chunks) + throw new HttpApiException("Invalid chunk id: Out of bounds", 400); + + var positionToSkipTo = ChunkSize * chunkId; var safePath = SanitizePath(path); var physicalPath = PathBuilder.File(BaseDirectory, safePath); @@ -80,15 +90,22 @@ public class FilesController : Controller if (!string.IsNullOrEmpty(baseDir)) Directory.CreateDirectory(baseDir); - await using var fs = System.IO.File.Create( - physicalPath - ); + var didExistBefore = System.IO.File.Exists(physicalPath); - await stream.CopyToAsync(fs); + await using var fs = System.IO.File.Open(physicalPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + // This creates the file in the correct size so we can handle the chunk if it didnt exist + if (!didExistBefore) + fs.SetLength(totalSize); + + fs.Position = positionToSkipTo; + + var dataStream = file.OpenReadStream(); + + await dataStream.CopyToAsync(fs); await fs.FlushAsync(); + fs.Close(); - stream.Close(); } [HttpPost("move")] @@ -150,8 +167,8 @@ public class FilesController : Controller return Task.CompletedTask; } - [HttpGet("read")] - public async Task Read([FromQuery] string path) + [HttpGet("download")] + public async Task Download([FromQuery] string path) { var safePath = SanitizePath(path); var physicalPath = PathBuilder.File(BaseDirectory, safePath); diff --git a/Moonlight.Client/Implementations/SysFileSystemProvider.cs b/Moonlight.Client/Implementations/SysFileSystemProvider.cs index edd3efaa..4e6ca06e 100644 --- a/Moonlight.Client/Implementations/SysFileSystemProvider.cs +++ b/Moonlight.Client/Implementations/SysFileSystemProvider.cs @@ -2,7 +2,6 @@ using MoonCore.Blazor.Tailwind.Fm; using MoonCore.Blazor.Tailwind.Fm.Models; using MoonCore.Blazor.Tailwind.Services; -using MoonCore.Blazor.Tailwind.Xhr; using MoonCore.Helpers; using Moonlight.Shared.Http.Requests.Admin.Sys.Files; using Moonlight.Shared.Http.Responses.Admin.Sys; @@ -14,7 +13,6 @@ public class SysFileSystemProvider : IFileSystemProvider, ICompressFileSystemPro private readonly DownloadService DownloadService; private readonly HttpApiClient HttpApiClient; private readonly LocalStorageService LocalStorageService; - private readonly XmlHttpClient XmlHttpClient; private readonly string BaseApiUrl = "api/admin/system/files"; public CompressType[] CompressTypes { get; } = @@ -31,12 +29,15 @@ public class SysFileSystemProvider : IFileSystemProvider, ICompressFileSystemPro } ]; - public SysFileSystemProvider(HttpApiClient httpApiClient, DownloadService downloadService, LocalStorageService localStorageService, XmlHttpClient xmlHttpClient) + public SysFileSystemProvider( + HttpApiClient httpApiClient, + DownloadService downloadService, + LocalStorageService localStorageService + ) { HttpApiClient = httpApiClient; DownloadService = downloadService; LocalStorageService = localStorageService; - XmlHttpClient = xmlHttpClient; } public async Task List(string path) @@ -57,11 +58,7 @@ public class SysFileSystemProvider : IFileSystemProvider, ICompressFileSystemPro public async Task Create(string path, Stream stream) { - var content = new MultipartFormDataContent(); - - content.Add(new StreamContent(stream), "file", "x"); - - await HttpApiClient.Post($"{BaseApiUrl}/create?path={path}", content); + await Upload(_ => Task.CompletedTask, path, stream); } public async Task Move(string oldPath, string newPath) @@ -74,45 +71,46 @@ public class SysFileSystemProvider : IFileSystemProvider, ICompressFileSystemPro => await HttpApiClient.Post($"{BaseApiUrl}/mkdir?path={path}"); public async Task Read(string path) - => await HttpApiClient.GetStream($"{BaseApiUrl}/read?path={path}"); + => await HttpApiClient.GetStream($"{BaseApiUrl}/download?path={path}"); - public async Task Download(Func updateProgress, string path, string fileName) + public async Task Download(Func updateProgress, string path, string fileName) { var accessToken = await LocalStorageService.GetString("AccessToken"); - - await DownloadService.DownloadUrl(fileName, $"{BaseApiUrl}/read?path={path}", async (bytes, _) => - { - await updateProgress.Invoke(bytes); - }, headers => - { - headers.Add("Authorization", $"Bearer {accessToken}"); - }); + + await DownloadService.DownloadUrl(fileName, $"{BaseApiUrl}/download?path={path}", + async (loaded, total) => + { + var percent = total == 0 ? 0 : (int)Math.Round((float)loaded / total * 100); + await updateProgress.Invoke(percent); + }, + onConfigureHeaders: headers => { headers.Add("Authorization", $"Bearer {accessToken}"); } + ); } - public async Task Upload(Func updateProgress, string path, Stream stream) + public async Task Upload(Func updateProgress, string path, Stream stream) { - var tcs = new TaskCompletionSource(); - var accessToken = await LocalStorageService.GetString("AccessToken"); + var size = stream.Length; + var chunkSize = ByteConverter.FromMegaBytes(20).Bytes; + + var chunks = size / chunkSize; + chunks += size % chunkSize > 0 ? 1 : 0; - await using var request = await XmlHttpClient.Create(); - - request.OnUploadProgress += async ev => + for (var chunkId = 0; chunkId < chunks; chunkId++) { - await updateProgress.Invoke(ev.Loaded); - }; + var percent = (int)Math.Round((chunkId + 1f) / chunks * 100); + await updateProgress.Invoke(percent); + + var buffer = new byte[chunkSize]; + var bytesRead = await stream.ReadAsync(buffer); - request.OnLoadend += _ => - { - tcs.SetResult(); - return Task.CompletedTask; - }; + var uploadForm = new MultipartFormDataContent(); + uploadForm.Add(new ByteArrayContent(buffer, 0, bytesRead), "file", path); - await request.Open("POST", $"{BaseApiUrl}/create?path={path}"); - await request.SetRequestHeader("Authorization", $"Bearer {accessToken}"); - - await request.SendFile(stream, "file", "file"); - - await tcs.Task; + await HttpApiClient.Post( + $"{BaseApiUrl}/upload?path={path}&totalSize={size}&chunkId={chunkId}", + uploadForm + ); + } } public async Task Compress(CompressType type, string path, string[] itemsToCompress) diff --git a/Moonlight.Client/Moonlight.Client.csproj b/Moonlight.Client/Moonlight.Client.csproj index deb398a2..e6f41d79 100644 --- a/Moonlight.Client/Moonlight.Client.csproj +++ b/Moonlight.Client/Moonlight.Client.csproj @@ -27,7 +27,7 @@ - +