diff --git a/DriveClient/Helpers/KDriveJsonContext.cs b/DriveClient/Helpers/KDriveJsonContext.cs index aaebc90..1892454 100644 --- a/DriveClient/Helpers/KDriveJsonContext.cs +++ b/DriveClient/Helpers/KDriveJsonContext.cs @@ -11,6 +11,7 @@ namespace kDriveClient.Helpers [JsonSerializable(typeof(KDriveFile))] [JsonSerializable(typeof(KDriveChunk))] [JsonSerializable(typeof(KDriveUploadResponseWraper))] + [JsonSerializable(typeof(KDriveFinishRequest))] [JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)] public partial class KDriveJsonContext : JsonSerializerContext { diff --git a/DriveClient/Helpers/KDriveRequestFactory.cs b/DriveClient/Helpers/KDriveRequestFactory.cs index 5b62359..ce81a4b 100644 --- a/DriveClient/Helpers/KDriveRequestFactory.cs +++ b/DriveClient/Helpers/KDriveRequestFactory.cs @@ -74,11 +74,8 @@ public static HttpRequestMessage CreateChunkUploadRequest(string baseUrl, string /// An HttpRequestMessage configured to finish the upload session. public static HttpRequestMessage CreateFinishSessionRequest(long driveId, string sessionToken, string totalChunkHash) { - var content = new StringContent(JsonSerializer.Serialize(new - { - total_chunk_hash = $"sha256:{totalChunkHash.ToLowerInvariant()}" - }, KDriveJsonContext.Default.Object)); - + // Send empty JSON body - kDrive validates file integrity via individual chunk hashes + var content = new StringContent("{}"); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); return new HttpRequestMessage(HttpMethod.Post, $"/3/drive/{driveId}/upload/session/{sessionToken}/finish") diff --git a/DriveClient/Models/KDriveFile.cs b/DriveClient/Models/KDriveFile.cs index 848447a..7fd2b91 100644 --- a/DriveClient/Models/KDriveFile.cs +++ b/DriveClient/Models/KDriveFile.cs @@ -1,4 +1,6 @@ -namespace kDriveClient.Models +using System.Text; + +namespace kDriveClient.Models { /// /// KDriveFile represents a file in the kDrive system. @@ -36,14 +38,17 @@ public class KDriveFile public string? SymbolicLink { get; set; } /// - /// TotalChunkHash is the SHA-256 hash of the entire file content, computed from all chunks. + /// TotalChunkHash is the SHA-256 hash of the entire file content. /// public string TotalChunkHash { get { - return Convert.ToHexString( - SHA256.HashData(this.Content)); + var originalPosition = this.Content.Position; + this.Content.Position = 0; + var hash = Convert.ToHexString(SHA256.HashData(this.Content)); + this.Content.Position = originalPosition; + return hash; } } @@ -82,7 +87,8 @@ public void SplitIntoChunks(int chunkSize) 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)); } this.Content.Position = 0; diff --git a/DriveClient/Models/KDriveFinishRequest.cs b/DriveClient/Models/KDriveFinishRequest.cs new file mode 100644 index 0000000..b02e2f9 --- /dev/null +++ b/DriveClient/Models/KDriveFinishRequest.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace kDriveClient.Models +{ + /// + /// Represents the request body for finishing an upload session. + /// + public class KDriveFinishRequest + { + /// + /// The total chunk hash of the uploaded file. + /// + [JsonPropertyName("total_chunk_hash")] + public string TotalChunkHash { get; set; } = string.Empty; + } +} diff --git a/DriveClient/kDriveClient/KDriveClient.cs b/DriveClient/kDriveClient/KDriveClient.cs index b3d0b06..c5666b0 100644 --- a/DriveClient/kDriveClient/KDriveClient.cs +++ b/DriveClient/kDriveClient/KDriveClient.cs @@ -81,7 +81,7 @@ public KDriveClient(string token, long driveId, ILogger? logger) : /// Choose if we should make a speed test to optimize chunks /// Number of parrallels threads /// Logger - public KDriveClient(string token, long driveId, bool autoChunk, int parallelism, ILogger? logger) : this(token, driveId, autoChunk, parallelism, logger, null) + public KDriveClient(string token, long driveId, bool autoChunk, int parallelism, ILogger? logger) : this(token, driveId, autoChunk, parallelism, logger, null, null) { } /// @@ -93,7 +93,8 @@ public KDriveClient(string token, long driveId, bool autoChunk, int parallelism, /// Number of parrallels threads /// Logger /// Custome HttpClient - public KDriveClient(string token, long driveId, bool autoChunk, int parallelism, ILogger? logger, HttpClient? httpClient = null) + /// Optional custom chunk size in bytes. If not specified, will be calculated dynamically based on speed test when autoChunk is true. + public KDriveClient(string token, long driveId, bool autoChunk, int parallelism, ILogger? logger, HttpClient? httpClient = null, int? chunkSize = null) { DriveId = driveId; Parallelism = parallelism; @@ -103,12 +104,25 @@ public KDriveClient(string token, long driveId, bool autoChunk, int parallelism, HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd("kDriveClient.NET/version"); this.Logger?.LogInformation("KDriveClient initialized with Drive ID: {DriveId}", DriveId); + + if (chunkSize.HasValue) + { + DynamicChunkSizeBytes = chunkSize.Value; + this.Logger?.LogInformation("Using custom chunk size: {ChunkSize} bytes", DynamicChunkSizeBytes); + } + if (autoChunk) { this.Logger?.LogInformation("Auto chunking enabled, initializing upload strategy..."); - InitializeUploadStrategyAsync().GetAwaiter().GetResult(); + InitializeUploadStrategyAsync(chunkSize).GetAwaiter().GetResult(); this.Logger?.LogInformation("Upload strategy initialized with direct upload threshold: {Threshold} bytes and dynamic chunk size: {ChunkSize} bytes", DirectUploadThresholdBytes, DynamicChunkSizeBytes); } + else if (!chunkSize.HasValue) + { + // If autoChunk is disabled and no custom chunk size provided, use a default + DynamicChunkSizeBytes = 1024 * 1024; // Default to 1MB chunks + this.Logger?.LogInformation("Using default chunk size: {ChunkSize} bytes", DynamicChunkSizeBytes); + } } /// diff --git a/DriveClient/kDriveClient/SpeedTest.cs b/DriveClient/kDriveClient/SpeedTest.cs index 753c025..97fb243 100644 --- a/DriveClient/kDriveClient/SpeedTest.cs +++ b/DriveClient/kDriveClient/SpeedTest.cs @@ -11,9 +11,10 @@ public partial class KDriveClient /// /// Initializes the upload strategy by performing a speed test. /// + /// Optional custom chunk size in bytes. If specified, only DirectUploadThresholdBytes will be calculated from speed test. /// Cancellation token to cancel the operation. /// A task that represents the asynchronous operation. - private async Task InitializeUploadStrategyAsync(CancellationToken ct = default) + private async Task InitializeUploadStrategyAsync(int? customChunkSize = null, CancellationToken ct = default) { this.Logger?.LogInformation("Starting upload strategy initialization..."); var buffer = new byte[1024 * 1024]; @@ -65,9 +66,18 @@ private async Task InitializeUploadStrategyAsync(CancellationToken ct = default) var speedBytesPerSec = buffer.Length / (sw.ElapsedMilliseconds / 1000.0); DirectUploadThresholdBytes = (long)speedBytesPerSec; - DynamicChunkSizeBytes = (int)(speedBytesPerSec * 0.9); - this.Logger?.LogInformation("Upload strategy initialized: DirectUploadThresholdBytes = {DirectUploadThresholdBytes}, DynamicChunkSizeBytes = {DynamicChunkSizeBytes}", - DirectUploadThresholdBytes, DynamicChunkSizeBytes); + + if (!customChunkSize.HasValue) + { + DynamicChunkSizeBytes = (int)(speedBytesPerSec * 0.9); + this.Logger?.LogInformation("Upload strategy initialized: DirectUploadThresholdBytes = {DirectUploadThresholdBytes}, DynamicChunkSizeBytes = {DynamicChunkSizeBytes} (calculated from speed test)", + DirectUploadThresholdBytes, DynamicChunkSizeBytes); + } + else + { + this.Logger?.LogInformation("Upload strategy initialized: DirectUploadThresholdBytes = {DirectUploadThresholdBytes}, DynamicChunkSizeBytes = {DynamicChunkSizeBytes} (custom)", + DirectUploadThresholdBytes, DynamicChunkSizeBytes); + } } } } \ No newline at end of file