diff --git a/LANCommander/Controllers/Api/UploadController.cs b/LANCommander/Controllers/Api/UploadController.cs
new file mode 100644
index 0000000..2469a97
--- /dev/null
+++ b/LANCommander/Controllers/Api/UploadController.cs
@@ -0,0 +1,51 @@
+using LANCommander.Models;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LANCommander.Controllers.Api
+{
+ [Route("api/[controller]")]
+ [ApiController]
+ public class UploadController : ControllerBase
+ {
+ private const string UploadDirectory = "Upload";
+
+ [HttpPost("Init")]
+ public string Init()
+ {
+ var key = Guid.NewGuid().ToString();
+
+ if (!Directory.Exists(UploadDirectory))
+ Directory.CreateDirectory(UploadDirectory);
+
+ if (!System.IO.File.Exists(Path.Combine(UploadDirectory, key)))
+ System.IO.File.Create(Path.Combine(UploadDirectory, key)).Close();
+
+ return key;
+ }
+
+ [HttpPost("Chunk")]
+ public async Task Chunk([FromForm] ChunkUpload chunk)
+ {
+ var filePath = Path.Combine(UploadDirectory, chunk.Key.ToString());
+
+ if (!System.IO.File.Exists(filePath))
+ throw new Exception("Destination file not initialized.");
+
+ Request.EnableBuffering();
+
+ using (var ms = new MemoryStream())
+ {
+ await chunk.File.CopyToAsync(ms);
+
+ var data = ms.ToArray();
+
+ using (var fs = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.None))
+ {
+ fs.Position = chunk.Start;
+ fs.Write(data, 0, data.Length);
+ }
+ }
+ }
+ }
+}
diff --git a/LANCommander/Pages/Games/Archives/Upload.razor b/LANCommander/Pages/Games/Archives/Upload.razor
new file mode 100644
index 0000000..e4f482d
--- /dev/null
+++ b/LANCommander/Pages/Games/Archives/Upload.razor
@@ -0,0 +1,104 @@
+@page "/Games/{id:guid}/Archives/Upload"
+@using System.Net;
+@inject HttpClient HttpClient
+@inject NavigationManager Navigator
+
+
+
+
+ Upload Archive
+
+
+
+
+Upload
+
+
+
+@code {
+ [Parameter] public Guid Id { get; set; }
+
+ IBrowserFile File { get; set; }
+
+ const int ChunkSize = 1024 * 1024 * 1;
+
+ int Progress = 0;
+ bool Uploading = false;
+
+ protected override async Task OnInitializedAsync()
+ {
+ HttpClient.BaseAddress = new Uri(Navigator.BaseUri);
+ }
+
+ private void FileSelected(IBrowserFile file)
+ {
+ File = file;
+ }
+
+ private async Task UploadArchive()
+ {
+ var initResponse = await HttpClient.PostAsync("api/Upload/Init", null);
+
+ Guid objectKey = Guid.Empty;
+
+ if (initResponse.StatusCode == HttpStatusCode.OK)
+ {
+ var responseKey = await initResponse.Content.ReadAsStringAsync();
+
+ Guid.TryParse(responseKey, out objectKey);
+ }
+
+ long uploadedBytes = 0;
+ long totalBytes = File.Size;
+
+ using (var stream = File.OpenReadStream(long.MaxValue))
+ {
+ Uploading = true;
+
+ while (Uploading)
+ {
+ byte[] chunk;
+
+ if (totalBytes - uploadedBytes < ChunkSize)
+ chunk = new byte[totalBytes - uploadedBytes];
+ else
+ chunk = new byte[ChunkSize];
+
+ await stream.ReadAsync(chunk, 0, chunk.Length);
+
+ using (var formFile = new MultipartFormDataContent())
+ {
+ var content = new StreamContent(new MemoryStream(chunk));
+
+ formFile.Add(content, "File", File.Name);
+ formFile.Add(new StringContent(uploadedBytes.ToString()), "Start");
+ formFile.Add(new StringContent((uploadedBytes + chunk.Length).ToString()), "End");
+ formFile.Add(new StringContent(objectKey.ToString()), "Key");
+ formFile.Add(new StringContent(totalBytes.ToString()), "Total");
+
+ var response = await HttpClient.PostAsync("api/Upload/Chunk", formFile);
+
+ if (response.StatusCode == HttpStatusCode.OK) {
+ uploadedBytes += chunk.Length;
+
+ Progress = (int)(uploadedBytes * 100 / totalBytes);
+
+ if (Progress >= 100)
+ Uploading = false;
+ }
+ else
+ {
+ Uploading = false;
+ // Error condition
+ }
+ }
+
+ await InvokeAsync(StateHasChanged);
+ }
+ }
+ }
+}