Compare commits

...
4 Commits
2 changed files with 33 additions and 23 deletions

No files matched your search

+19 -15
View File
@@ -33,7 +33,7 @@ app.UseStaticFiles(new StaticFileOptions
app.UseStatusCodePages(); app.UseStatusCodePages();
app.MapPost("/Files/{fileId}", (string fileId, FileService fileService) => app.MapPost("/Files/{fileId}", (string fileId, CreateFileRequest request, FileService fileService) =>
{ {
if (fileService.IsValidFileId(fileId) is false) if (fileService.IsValidFileId(fileId) is false)
{ {
@@ -49,6 +49,10 @@ app.MapPost("/Files/{fileId}", (string fileId, FileService fileService) =>
Directory.CreateDirectory(fileDirectory); Directory.CreateDirectory(fileDirectory);
var partFilePath = Path.Combine(fileDirectory, "upload.part");
using var handle = File.OpenHandle(partFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.ReadWrite);
RandomAccess.SetLength(handle, request.Size);
return Results.Ok(new SessionResponse(fileId, "New upload session created.")); return Results.Ok(new SessionResponse(fileId, "New upload session created."));
}); });
@@ -117,7 +121,10 @@ app.MapPost("/Files/{fileId}/upload", async (
} }
using var formStream = chunk.OpenReadStream(); using var formStream = chunk.OpenReadStream();
var hashBytes = await SHA256.HashDataAsync(formStream); var buffer = new byte[chunk.Length];
await formStream.ReadExactlyAsync(buffer);
var hashBytes = SHA256.HashData(buffer);
var calculatedHash = Convert.ToHexString(hashBytes).ToLowerInvariant(); var calculatedHash = Convert.ToHexString(hashBytes).ToLowerInvariant();
if (CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(calculatedHash), Encoding.UTF8.GetBytes(hash.ToLowerInvariant())) is false) if (CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(calculatedHash), Encoding.UTF8.GetBytes(hash.ToLowerInvariant())) is false)
@@ -128,20 +135,15 @@ app.MapPost("/Files/{fileId}/upload", async (
var partFilePath = Path.Combine(fileDirectory, "upload.part"); var partFilePath = Path.Combine(fileDirectory, "upload.part");
long byteOffset = index * chunkSize; long byteOffset = index * chunkSize;
var fileStreamOptions = new FileStreamOptions using var handle = File.OpenHandle(
{ partFilePath,
Mode = FileMode.OpenOrCreate, FileMode.Open,
Access = FileAccess.Write, FileAccess.Write,
Share = FileShare.ReadWrite, FileShare.ReadWrite,
BufferSize = 81920, FileOptions.Asynchronous
Options = FileOptions.Asynchronous );
};
formStream.Position = 0; await RandomAccess.WriteAsync(handle, buffer, byteOffset);
using var targetStream = new FileStream(partFilePath, fileStreamOptions);
targetStream.Seek(byteOffset, SeekOrigin.Begin);
await formStream.CopyToAsync(targetStream);
await File.WriteAllTextAsync(markerFilePath, string.Empty); await File.WriteAllTextAsync(markerFilePath, string.Empty);
@@ -256,6 +258,7 @@ app.MapDelete("/Files/{fileId}", (string fileId, FileService fileService) =>
app.Run(); app.Run();
record CreateFileRequest(long Size);
record FileCompleteRequest(string Filename, string CompositeHash); record FileCompleteRequest(string Filename, string CompositeHash);
record SessionResponse(string FileId, string Message); record SessionResponse(string FileId, string Message);
record ChunkStatusInfo(int Index, string Hash); record ChunkStatusInfo(int Index, string Hash);
@@ -263,6 +266,7 @@ record SessionStatusResponse(string FileId, List<ChunkStatusInfo> UploadedChunks
record ChunkUploadResponse(string FileId, int Index, string Status); record ChunkUploadResponse(string FileId, int Index, string Status);
record CompleteResponse(string Status, string Path); record CompleteResponse(string Status, string Path);
[JsonSerializable(typeof(CreateFileRequest))]
[JsonSerializable(typeof(FileCompleteRequest))] [JsonSerializable(typeof(FileCompleteRequest))]
[JsonSerializable(typeof(SessionResponse))] [JsonSerializable(typeof(SessionResponse))]
[JsonSerializable(typeof(ChunkStatusInfo))] [JsonSerializable(typeof(ChunkStatusInfo))]
+14 -8
View File
@@ -56,7 +56,12 @@ document.addEventListener("DOMContentLoaded", () => {
const createFileResponse = await fetch(`/files/${fileId}`, { const createFileResponse = await fetch(`/files/${fileId}`, {
method: "POST", method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({ size: file.size }),
}); });
let existingChunksMap = new Map(); let existingChunksMap = new Map();
if (createFileResponse.status === 409 || createFileResponse.ok) { if (createFileResponse.status === 409 || createFileResponse.ok) {
@@ -112,10 +117,10 @@ document.addEventListener("DOMContentLoaded", () => {
: "Uploading..."; : "Uploading...";
const MAX_CONCURRENCY = 4; const MAX_CONCURRENCY = 4;
let uploadFailed = false; let failedChunksCount = 0;
async function worker() { async function worker() {
while (pendingIndices.length > 0 && !uploadFailed) { while (pendingIndices.length > 0) {
const index = pendingIndices.shift(); const index = pendingIndices.shift();
const start = index * CHUNK_SIZE; const start = index * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size); const end = Math.min(start + CHUNK_SIZE, file.size);
@@ -144,7 +149,9 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`Chunk ${index} failed`); throw new Error(
`Chunk ${index} failed with status ${response.status}`,
);
} }
chunkCheckmark?.classList.remove("hidden"); chunkCheckmark?.classList.remove("hidden");
@@ -154,8 +161,8 @@ document.addEventListener("DOMContentLoaded", () => {
chunkStatus.innerText = "Succeeded"; chunkStatus.innerText = "Succeeded";
} }
} catch (err) { } catch (err) {
console.err(err); console.error(`Chunk ${index} failed:`, err);
uploadFailed = true; failedChunksCount++;
chunkRedX?.classList.remove("hidden"); chunkRedX?.classList.remove("hidden");
chunkSpinner?.classList.add("hidden"); chunkSpinner?.classList.add("hidden");
@@ -176,11 +183,10 @@ document.addEventListener("DOMContentLoaded", () => {
await Promise.all(workers); await Promise.all(workers);
if (uploadFailed) { if (failedChunksCount > 0) {
fileSpinnerElement?.classList.add("hidden"); fileSpinnerElement?.classList.add("hidden");
fileRedXElement?.classList.remove("hidden"); fileRedXElement?.classList.remove("hidden");
fileStatusElement.innerText = fileStatusElement.innerText = `${failedChunksCount} chunk(s) failed. Select file again to resume.`;
"Upload paused/failed. Select file again to resume.";
return; return;
} }