diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs index a64747c..5d32034 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Security.Cryptography; using System.Text.Json; using Jibo.Cloud.Application.Abstractions; using Jibo.Cloud.Domain.Models; @@ -343,10 +344,15 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia var meta = ReadObject(body, "meta") ?? new Dictionary(StringComparer.OrdinalIgnoreCase); var contentType = ReadHeader(envelope, "Content-Type") ?? "application/octet-stream"; meta["contentType"] = contentType; + var bodyBytes = string.IsNullOrWhiteSpace(envelope.BodyText) + ? [] + : Encoding.UTF8.GetBytes(envelope.BodyText); + meta["contentLength"] = bodyBytes.Length; + meta["contentSha256"] = Convert.ToHexString(SHA256.HashData(bodyBytes)).ToLowerInvariant(); if (!string.IsNullOrWhiteSpace(envelope.BodyText)) meta["bodyText"] = envelope.BodyText; _mediaContentStore.StoreAsync(path, contentType, - string.IsNullOrWhiteSpace(envelope.BodyText) ? [] : Encoding.UTF8.GetBytes(envelope.BodyText), + bodyBytes, meta as IReadOnlyDictionary, CancellationToken.None).GetAwaiter().GetResult(); return ProtocolDispatchResult.Ok( @@ -743,4 +749,4 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia return Task.FromResult(null); } } -} \ No newline at end of file +} diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Media/AzureBlobMediaContentStore.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Media/AzureBlobMediaContentStore.cs index abddb0a..b2304da 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Media/AzureBlobMediaContentStore.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Media/AzureBlobMediaContentStore.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Security.Cryptography; using Azure.Storage.Blobs; using Jibo.Cloud.Application.Abstractions; @@ -31,11 +32,17 @@ internal sealed class AzureBlobMediaContentStore : IMediaContentStore var metaBlob = _containerClient.GetBlobClient($"{relative}.json"); await _containerClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken); await contentBlob.UploadAsync(new MemoryStream(content), true, cancellationToken); + var manifestMeta = meta is null + ? new Dictionary(StringComparer.OrdinalIgnoreCase) + : new Dictionary(meta, StringComparer.OrdinalIgnoreCase); + manifestMeta["contentLength"] = content.Length; + manifestMeta["contentSha256"] = Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant(); + manifestMeta["storedUtc"] = DateTimeOffset.UtcNow; var payload = JsonSerializer.Serialize(new { path, contentType, - meta + meta = manifestMeta }, JsonOptions); await metaBlob.UploadAsync(BinaryData.FromString(payload), true, cancellationToken); } @@ -77,4 +84,4 @@ internal sealed class AzureBlobMediaContentStore : IMediaContentStore Meta = meta as IReadOnlyDictionary ?? new Dictionary(meta) }; } -} \ No newline at end of file +} diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Media/FileMediaContentStore.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Media/FileMediaContentStore.cs index 5ce97a9..a8cd896 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Media/FileMediaContentStore.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Media/FileMediaContentStore.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Security.Cryptography; using Jibo.Cloud.Application.Abstractions; namespace Jibo.Cloud.Infrastructure.Media; @@ -29,11 +30,17 @@ internal sealed class FileMediaContentStore : IMediaContentStore Directory.CreateDirectory(Path.GetDirectoryName(contentPath)!); await File.WriteAllBytesAsync(contentPath, content, cancellationToken); + var manifestMeta = meta is null + ? new Dictionary(StringComparer.OrdinalIgnoreCase) + : new Dictionary(meta, StringComparer.OrdinalIgnoreCase); + manifestMeta["contentLength"] = content.Length; + manifestMeta["contentSha256"] = Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant(); + manifestMeta["storedUtc"] = DateTimeOffset.UtcNow; var payload = new { path, contentType, - meta + meta = manifestMeta }; await File.WriteAllTextAsync(metaPath, JsonSerializer.Serialize(payload, JsonOptions), cancellationToken); } @@ -79,4 +86,4 @@ internal sealed class FileMediaContentStore : IMediaContentStore Meta = meta as IReadOnlyDictionary ?? new Dictionary(meta) }; } -} \ No newline at end of file +} diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/JiboCloudProtocolServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/JiboCloudProtocolServiceTests.cs index 3e13553..ff28a7c 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/JiboCloudProtocolServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/JiboCloudProtocolServiceTests.cs @@ -1,3 +1,5 @@ +using System.Security.Cryptography; +using System.Text; using System.Text.Json; using Jibo.Cloud.Application.Services; using Jibo.Cloud.Domain.Models; @@ -420,6 +422,46 @@ public sealed class JiboCloudProtocolServiceTests Assert.Equal("binary-photo-placeholder", mediaGet.BodyText); } + [Fact] + public async Task MediaCreate_WritesBinaryManifestMetadataForSync() + { + var directoryPath = Path.Combine(Path.GetTempPath(), "OpenJibo.Media.Tests", Guid.NewGuid().ToString("N")); + var service = new JiboCloudProtocolService(new InMemoryCloudStateStore(), + new FileMediaContentStore(directoryPath)); + const string bodyText = "binary-photo-placeholder"; + + var result = await service.DispatchAsync(new ProtocolEnvelope + { + HostName = "api.jibo.com", + Method = "POST", + ServicePrefix = "Media_20160725", + Operation = "Create", + Headers = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Content-Type"] = "image/jpeg", + ["x-path"] = "photo-blob-manifest", + ["x-type"] = "image" + }, + BodyText = bodyText + }); + + using var createdPayload = JsonDocument.Parse(result.BodyText); + var meta = createdPayload.RootElement.GetProperty("meta"); + Assert.Equal(bodyText.Length, meta.GetProperty("contentLength").GetInt32()); + Assert.Equal( + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(bodyText))).ToLowerInvariant(), + meta.GetProperty("contentSha256").GetString()); + + var metaPath = Path.Combine(directoryPath, "photo-blob-manifest.json"); + using var manifest = JsonDocument.Parse(await File.ReadAllTextAsync(metaPath)); + var manifestMeta = manifest.RootElement.GetProperty("meta"); + Assert.Equal(bodyText.Length, manifestMeta.GetProperty("contentLength").GetInt32()); + Assert.Equal( + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(bodyText))).ToLowerInvariant(), + manifestMeta.GetProperty("contentSha256").GetString()); + Assert.True(manifestMeta.TryGetProperty("storedUtc", out _)); + } + [Fact] public async Task KeyCreateSymmetricKey_ReturnsKeyPayload() { @@ -468,4 +510,4 @@ public sealed class JiboCloudProtocolServiceTests Assert.Contains(people, person => string.Equals(person.LoopId, store.GetLoops()[0].LoopId, StringComparison.OrdinalIgnoreCase)); } -} \ No newline at end of file +}