Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion DriveClient/Helpers/KDriveJsonContext.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using kDriveClient.Models;
using kDriveClient.Models.Exceptions;

namespace kDriveClient.Helpers
{
/// <summary>
Expand All @@ -11,8 +12,10 @@ namespace kDriveClient.Helpers
[JsonSerializable(typeof(KDriveFile))]
[JsonSerializable(typeof(KDriveChunk))]
[JsonSerializable(typeof(KDriveUploadResponseWraper))]
[JsonSerializable(typeof(KDriveFinishRequest))]
[JsonSerializable(typeof(KDriveUploadDataResponse))]
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
public partial class KDriveJsonContext : JsonSerializerContext
{
}
}
}
9 changes: 4 additions & 5 deletions DriveClient/Helpers/KDriveJsonHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static class KDriveJsonHelper
/// <exception cref="InvalidOperationException"></exception>
public static KDriveUploadResponse DeserializeUploadResponse(string json)
{
return JsonSerializer.Deserialize(json, KDriveJsonContext.Default.KDriveUploadResponseWraper)?.Data ?? throw new InvalidOperationException("Failed to parse upload response");
return JsonSerializer.Deserialize(json, KDriveJsonContext.Default.KDriveUploadResponseWraper)?.Data?.File ?? throw new InvalidOperationException("Failed to parse upload response");
}

/// <summary>
Expand Down Expand Up @@ -54,19 +54,18 @@ public static async Task<HttpResponseMessage> DeserializeResponseAsync(HttpRespo
{
if (!response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync(ct);

KDriveErrorResponse? error = null;
try
{
error = JsonSerializer.Deserialize(json, KDriveJsonContext.Default.KDriveErrorResponse);
await using var stream = await response.Content.ReadAsStreamAsync(ct);
error = await JsonSerializer.DeserializeAsync(stream, KDriveJsonContext.Default.KDriveErrorResponse, cancellationToken: ct);
}
catch
{
response.EnsureSuccessStatusCode();
}

if (error != null)
if (error is not null)
{
throw new KDriveApiException(error);
}
Expand Down
21 changes: 11 additions & 10 deletions DriveClient/Helpers/KDriveRequestFactory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using kDriveClient.Models;
using System.Net;
using System.Net.Http.Headers;

namespace kDriveClient.Helpers
Expand All @@ -19,7 +20,7 @@ public static HttpRequestMessage CreateUploadDirectRequest(long driveId, KDriveF
var url = $"/3/drive/{driveId}/upload?" + string.Join("&", BuildUploadQueryParams(file).ToList().ConvertAll(e => $"{e.Key}={e.Value}"));
var request = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = new StreamContent(new MemoryStream(file.Chunks.First().Content))
Content = new StreamContent(new MemoryStream(file.Chunks.First().Content!))
};

request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
Expand Down Expand Up @@ -55,14 +56,14 @@ public static HttpRequestMessage CreateStartSessionRequest(long driveId, KDriveF
/// <returns>An HttpRequestMessage configured for the chunk upload operation.</returns>
public static HttpRequestMessage CreateChunkUploadRequest(string baseUrl, string token, long driveId, KDriveChunk chunk)
{
var request = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/3/drive/{driveId}/upload/session/{token}/chunk?chunk_number={chunk.ChunkNumber + 1}&chunk_size={chunk.ChunkSize}&chunk_hash=sha256:{chunk.ChunkHash.ToLowerInvariant()}")
var content = new ReadOnlyMemoryContent(new ReadOnlyMemory<byte>(chunk.Content, 0, chunk.ChunkSize));
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
return new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/3/drive/{driveId}/upload/session/{token}/chunk?chunk_number={chunk.ChunkNumber + 1}&chunk_size={chunk.ChunkSize}&chunk_hash=sha256:{chunk.ChunkHash.ToLowerInvariant()}")
{
Content = new ByteArrayContent(chunk.Content)
Content = content,
Version = HttpVersion.Version20,
VersionPolicy = HttpVersionPolicy.RequestVersionOrLower,
};
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
request.Content.Headers.ContentLength = chunk.ChunkSize;

return request;
}

/// <summary>
Expand All @@ -74,10 +75,10 @@ public static HttpRequestMessage CreateChunkUploadRequest(string baseUrl, string
/// <returns>An HttpRequestMessage configured to finish the upload session.</returns>
public static HttpRequestMessage CreateFinishSessionRequest(long driveId, string sessionToken, string totalChunkHash)
{
var content = new StringContent(JsonSerializer.Serialize(new
var content = new StringContent(JsonSerializer.Serialize(new KDriveFinishRequest
{
total_chunk_hash = $"sha256:{totalChunkHash.ToLowerInvariant()}"
}, KDriveJsonContext.Default.Object));
TotalChunkHash = $"sha256:{totalChunkHash.ToLowerInvariant()}"
}, KDriveJsonContext.Default.KDriveFinishRequest));

content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

Expand Down
6 changes: 4 additions & 2 deletions DriveClient/Models/ConflictChoice.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
namespace kDriveClient.Models
{
/// <summary>
///
///
/// </summary>
public enum ConflictChoice
{
/// <summary>
/// Throw an error
/// </summary>
Error,

/// <summary>
/// Add a new version to file
/// </summary>
Version,

/// <summary>
/// Create a new file with an automated renaming
/// </summary>
Rename,
}
}
}
23 changes: 20 additions & 3 deletions DriveClient/Models/KDriveChunk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/// <summary>
/// kDriveChunk represents a chunk of Data in a kDrive file.
/// </summary>
public class KDriveChunk(byte[] content, int chunkNumber, byte[] hash)
public class KDriveChunk(byte[] content, int chunkNumber, byte[] hash) : IDisposable
{
/// <summary>
/// ChunkHash is the SHA-256 hash of the chunk content.
Expand All @@ -18,11 +18,28 @@ public class KDriveChunk(byte[] content, int chunkNumber, byte[] hash)
/// <summary>
/// ChunkSize is the size of the chunk in bytes.
/// </summary>
public long ChunkSize => this.Content.Length;
public int ChunkSize { get; set; } = content.Length;

/// <summary>
/// Content is the actual byte content of the chunk.
/// </summary>
public byte[] Content { get; init; } = content;
public byte[]? Content { get; private set; } = content;

/// <summary>
/// Frees resources used by the KDriveChunk.
/// </summary>
public void Dispose()
{
GC.SuppressFinalize(this);
}

/// <summary>
/// Deletes the content of the chunk to free memory.
/// </summary>
internal void Clean()
{
this.Content = null;
GC.Collect();
}
}
}
57 changes: 43 additions & 14 deletions DriveClient/Models/KDriveFile.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
namespace kDriveClient.Models
using System.Text;

namespace kDriveClient.Models
{
/// <summary>
/// KDriveFile represents a file in the kDrive system.
/// </summary>
public class KDriveFile
public class KDriveFile : IDisposable
{
private Int64 totalSize;

/// <summary>
/// CreatedAt is the timestamp when the file was created.
/// </summary>
Expand Down Expand Up @@ -38,20 +42,23 @@ public class KDriveFile
/// <summary>
/// TotalChunkHash is the SHA-256 hash of the entire file content, computed from all chunks.
/// </summary>
public string TotalChunkHash
public string TotalChunkHash { get; set; } = string.Empty;

/// <summary>
/// TotalSize is the total size of the file in bytes, calculated as the sum of all chunk sizes.
/// </summary>
public long TotalSize
{
get
{
return Convert.ToHexString(
SHA256.HashData(this.Content));
if (totalSize == 0)
{
totalSize = this.Chunks.Sum(c => (long)c.ChunkSize);
}
return totalSize;
}
}

/// <summary>
/// TotalSize is the total size of the file in bytes, calculated as the sum of all chunk sizes.
/// </summary>
public long TotalSize => this.Chunks.Sum(c => c.ChunkSize);

/// <summary>
/// Chunks is a list of KDriveChunk objects representing the file's content split into chunks.
/// </summary>
Expand All @@ -60,7 +67,7 @@ public string TotalChunkHash
/// <summary>
/// Content is a stream representing the file's content.
/// </summary>
public required Stream Content { get; init; }
public Stream? Content { get; init; }

/// <summary>
/// In case of conflict with an existing file, it define how to manage the conflict
Expand All @@ -73,19 +80,31 @@ public string TotalChunkHash
/// <param name="chunkSize">Define the size of each chunk (except the last one)</param>
public void SplitIntoChunks(int chunkSize)
{
if (this.Content == null)
{
throw new InvalidOperationException("Content stream is null.");
}

var buffer = new byte[chunkSize];
int chunkNumber = 0;
int bytesRead;

this.Content.Position = 0;

using var fileSha256 = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
while ((bytesRead = this.Content.Read(buffer, 0, chunkSize)) > 0)
{
byte[] content = [.. buffer.Take(bytesRead)];
this.Chunks.Add(new KDriveChunk(content, chunkNumber++, SHA256.HashData(content)));
var chunkHash = SHA256.HashData(content);
this.Chunks.Add(new KDriveChunk(content, chunkNumber++, chunkHash));
fileSha256.AppendData(Encoding.UTF8.GetBytes(Convert.ToHexString(chunkHash).ToLowerInvariant()));
}

this.Content.Position = 0;
this.Content.Dispose();
GC.Collect();

this.TotalChunkHash = this.Chunks.Count > 1 ?
this.TotalChunkHash = Convert.ToHexString(fileSha256.GetHashAndReset())
: this.Chunks.First().ChunkHash;
}

/// <summary>
Expand All @@ -111,5 +130,15 @@ public string ConvertConflictChoice()
_ => "error"
};
}

/// <summary>
/// Frees resources used by the KDriveFile instance.
/// </summary>
public void Dispose()
{
this.Chunks.ForEach(c => c.Dispose());

GC.SuppressFinalize(this);
}
}
}
14 changes: 14 additions & 0 deletions DriveClient/Models/KDriveFinishRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace kDriveClient.Models
{
/// <summary>
/// Represents the request body for finishing an upload session.
/// </summary>
public class KDriveFinishRequest
{
/// <summary>
/// The total chunk hash of the uploaded file.
/// </summary>
[JsonPropertyName("total_chunk_hash")]
public string TotalChunkHash { get; set; } = string.Empty;
}
}
Loading