Add binary media manifest metadata

This commit is contained in:
Jacob Dubin
2026-05-21 16:41:23 -05:00
parent 791fe60612
commit febceecab8
4 changed files with 69 additions and 7 deletions

View File

@@ -1,4 +1,5 @@
using System.Text; using System.Text;
using System.Security.Cryptography;
using System.Text.Json; using System.Text.Json;
using Jibo.Cloud.Application.Abstractions; using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Domain.Models; using Jibo.Cloud.Domain.Models;
@@ -343,10 +344,15 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia
var meta = ReadObject(body, "meta") ?? new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase); var meta = ReadObject(body, "meta") ?? new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
var contentType = ReadHeader(envelope, "Content-Type") ?? "application/octet-stream"; var contentType = ReadHeader(envelope, "Content-Type") ?? "application/octet-stream";
meta["contentType"] = contentType; 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; if (!string.IsNullOrWhiteSpace(envelope.BodyText)) meta["bodyText"] = envelope.BodyText;
_mediaContentStore.StoreAsync(path, contentType, _mediaContentStore.StoreAsync(path, contentType,
string.IsNullOrWhiteSpace(envelope.BodyText) ? [] : Encoding.UTF8.GetBytes(envelope.BodyText), bodyBytes,
meta as IReadOnlyDictionary<string, object?>, CancellationToken.None).GetAwaiter().GetResult(); meta as IReadOnlyDictionary<string, object?>, CancellationToken.None).GetAwaiter().GetResult();
return ProtocolDispatchResult.Ok( return ProtocolDispatchResult.Ok(
@@ -743,4 +749,4 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia
return Task.FromResult<MediaContentSnapshot?>(null); return Task.FromResult<MediaContentSnapshot?>(null);
} }
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Text.Json; using System.Text.Json;
using System.Security.Cryptography;
using Azure.Storage.Blobs; using Azure.Storage.Blobs;
using Jibo.Cloud.Application.Abstractions; using Jibo.Cloud.Application.Abstractions;
@@ -31,11 +32,17 @@ internal sealed class AzureBlobMediaContentStore : IMediaContentStore
var metaBlob = _containerClient.GetBlobClient($"{relative}.json"); var metaBlob = _containerClient.GetBlobClient($"{relative}.json");
await _containerClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken); await _containerClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
await contentBlob.UploadAsync(new MemoryStream(content), true, cancellationToken); await contentBlob.UploadAsync(new MemoryStream(content), true, cancellationToken);
var manifestMeta = meta is null
? new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, object?>(meta, StringComparer.OrdinalIgnoreCase);
manifestMeta["contentLength"] = content.Length;
manifestMeta["contentSha256"] = Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant();
manifestMeta["storedUtc"] = DateTimeOffset.UtcNow;
var payload = JsonSerializer.Serialize(new var payload = JsonSerializer.Serialize(new
{ {
path, path,
contentType, contentType,
meta meta = manifestMeta
}, JsonOptions); }, JsonOptions);
await metaBlob.UploadAsync(BinaryData.FromString(payload), true, cancellationToken); await metaBlob.UploadAsync(BinaryData.FromString(payload), true, cancellationToken);
} }
@@ -77,4 +84,4 @@ internal sealed class AzureBlobMediaContentStore : IMediaContentStore
Meta = meta as IReadOnlyDictionary<string, object?> ?? new Dictionary<string, object?>(meta) Meta = meta as IReadOnlyDictionary<string, object?> ?? new Dictionary<string, object?>(meta)
}; };
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Text.Json; using System.Text.Json;
using System.Security.Cryptography;
using Jibo.Cloud.Application.Abstractions; using Jibo.Cloud.Application.Abstractions;
namespace Jibo.Cloud.Infrastructure.Media; namespace Jibo.Cloud.Infrastructure.Media;
@@ -29,11 +30,17 @@ internal sealed class FileMediaContentStore : IMediaContentStore
Directory.CreateDirectory(Path.GetDirectoryName(contentPath)!); Directory.CreateDirectory(Path.GetDirectoryName(contentPath)!);
await File.WriteAllBytesAsync(contentPath, content, cancellationToken); await File.WriteAllBytesAsync(contentPath, content, cancellationToken);
var manifestMeta = meta is null
? new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, object?>(meta, StringComparer.OrdinalIgnoreCase);
manifestMeta["contentLength"] = content.Length;
manifestMeta["contentSha256"] = Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant();
manifestMeta["storedUtc"] = DateTimeOffset.UtcNow;
var payload = new var payload = new
{ {
path, path,
contentType, contentType,
meta meta = manifestMeta
}; };
await File.WriteAllTextAsync(metaPath, JsonSerializer.Serialize(payload, JsonOptions), cancellationToken); await File.WriteAllTextAsync(metaPath, JsonSerializer.Serialize(payload, JsonOptions), cancellationToken);
} }
@@ -79,4 +86,4 @@ internal sealed class FileMediaContentStore : IMediaContentStore
Meta = meta as IReadOnlyDictionary<string, object?> ?? new Dictionary<string, object?>(meta) Meta = meta as IReadOnlyDictionary<string, object?> ?? new Dictionary<string, object?>(meta)
}; };
} }
} }

View File

@@ -1,3 +1,5 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json; using System.Text.Json;
using Jibo.Cloud.Application.Services; using Jibo.Cloud.Application.Services;
using Jibo.Cloud.Domain.Models; using Jibo.Cloud.Domain.Models;
@@ -420,6 +422,46 @@ public sealed class JiboCloudProtocolServiceTests
Assert.Equal("binary-photo-placeholder", mediaGet.BodyText); 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<string, string>(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] [Fact]
public async Task KeyCreateSymmetricKey_ReturnsKeyPayload() public async Task KeyCreateSymmetricKey_ReturnsKeyPayload()
{ {
@@ -468,4 +510,4 @@ public sealed class JiboCloudProtocolServiceTests
Assert.Contains(people, Assert.Contains(people,
person => string.Equals(person.LoopId, store.GetLoops()[0].LoopId, StringComparison.OrdinalIgnoreCase)); person => string.Equals(person.LoopId, store.GetLoops()[0].LoopId, StringComparison.OrdinalIgnoreCase));
} }
} }