Compare commits
4
Commits
c64215bb2f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91de99cde2 | ||
|
|
dcbbe04eeb | ||
|
|
c5c7e26f4e | ||
|
|
7b5d604e61 |
No files matched your search
+19
-15
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user