Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ public BlobDownloadDetails() { }
public string AcceptRanges { get { throw null; } }
public int BlobCommittedBlockCount { get { throw null; } }
public byte[] BlobContentHash { get { throw null; } }
public bool BlobSealed { get { throw null; } }
public long BlobSequenceNumber { get { throw null; } }
public string CacheControl { get { throw null; } }
public string ContentDisposition { get { throw null; } }
Expand Down Expand Up @@ -1038,6 +1039,8 @@ public AppendBlobClient(System.Uri blobUri, Azure.Storage.StorageSharedKeyCreden
public virtual System.Threading.Tasks.Task<Azure.Response<Azure.Storage.Blobs.Models.BlobContentInfo>> CreateAsync(Azure.Storage.Blobs.Models.BlobHttpHeaders httpHeaders = null, System.Collections.Generic.IDictionary<string, string> metadata = null, Azure.Storage.Blobs.Models.AppendBlobRequestConditions conditions = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual Azure.Response<Azure.Storage.Blobs.Models.BlobContentInfo> CreateIfNotExists(Azure.Storage.Blobs.Models.BlobHttpHeaders httpHeaders = null, System.Collections.Generic.IDictionary<string, string> metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual System.Threading.Tasks.Task<Azure.Response<Azure.Storage.Blobs.Models.BlobContentInfo>> CreateIfNotExistsAsync(Azure.Storage.Blobs.Models.BlobHttpHeaders httpHeaders = null, System.Collections.Generic.IDictionary<string, string> metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual Azure.Response<Azure.Storage.Blobs.Models.BlobInfo> Seal(Azure.Storage.Blobs.Models.AppendBlobRequestConditions conditions = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual System.Threading.Tasks.Task<Azure.Response<Azure.Storage.Blobs.Models.BlobInfo>> SealAsync(Azure.Storage.Blobs.Models.AppendBlobRequestConditions conditions = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public new Azure.Storage.Blobs.Specialized.AppendBlobClient WithSnapshot(string snapshot) { throw null; }
}
public partial class BlobBaseClient
Expand Down
127 changes: 127 additions & 0 deletions sdk/storage/Azure.Storage.Blobs/src/AppendBlobClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,133 @@ private async Task<Response<BlobAppendInfo>> AppendBlockFromUriInternal(
}
}
#endregion AppendBlockFromUri

#region Seal
/// <summary>
/// Seals the append blob, making it read only.
/// Any subsequent appends will fail.
/// </summary>
/// <param name="conditions">
/// Optional <see cref="AppendBlobRequestConditions"/> to add
/// conditions on the sealing of this blob.
/// </param>
/// <param name="cancellationToken">
/// Optional <see cref="CancellationToken"/> to propagate
/// notifications that the operation should be cancelled.
/// </param>
/// <returns>
/// A <see cref="Response{BlobInfo}"/> describing the
/// state of the sealed append blob.
/// </returns>
/// <remarks>
/// A <see cref="RequestFailedException"/> will be thrown if
/// a failure occurs.
/// </remarks>
public virtual Response<BlobInfo> Seal(
AppendBlobRequestConditions conditions = default,
CancellationToken cancellationToken = default)
=> SealInternal(
conditions,
async: false,
cancellationToken: cancellationToken)
.EnsureCompleted();

/// <summary>
/// Seals the append blob, making it read only.
/// Any subsequent appends will fail.
/// </summary>
/// <param name="conditions">
/// Optional <see cref="AppendBlobRequestConditions"/> to add
/// conditions on the sealing of this blob.
/// </param>
/// <param name="cancellationToken">
/// Optional <see cref="CancellationToken"/> to propagate
/// notifications that the operation should be cancelled.
/// </param>
/// <returns>
/// A <see cref="Response{BlobInfo}"/> describing the
/// state of the sealed append blob.
/// </returns>
/// <remarks>
/// A <see cref="RequestFailedException"/> will be thrown if
/// a failure occurs.
/// </remarks>
public virtual async Task<Response<BlobInfo>> SealAsync(
AppendBlobRequestConditions conditions = default,
CancellationToken cancellationToken = default)
=> await SealInternal(
conditions,
async: true,
cancellationToken: cancellationToken)
.ConfigureAwait(false);

/// <summary>
/// Seals the append blob, making it read only.
/// Any subsequent appends will fail.
/// </summary>
/// <param name="conditions">
/// Optional <see cref="AppendBlobRequestConditions"/> to add
/// conditions on the sealing of this blob.
/// </param>
/// <param name="async">
/// Whether to invoke the operation asynchronously.
/// </param>
/// <param name="cancellationToken">
/// Optional <see cref="CancellationToken"/> to propagate
/// notifications that the operation should be cancelled.
/// </param>
/// <returns>
/// A <see cref="Response{BlobInfo}"/> describing the
/// state of the sealed append blob.
/// </returns>
/// <remarks>
/// A <see cref="RequestFailedException"/> will be thrown if
/// a failure occurs.
/// </remarks>
private async Task<Response<BlobInfo>> SealInternal(
AppendBlobRequestConditions conditions,
bool async,
CancellationToken cancellationToken)
{
using (Pipeline.BeginLoggingScope(nameof(AppendBlobClient)))
{
try
{
Response<AppendBlobSealInternal> response = await BlobRestClient.AppendBlob.SealAsync(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not seeing AppendBlobSealInternal defined in this PR. Are there any additional interesting properties outside of what's provided by BlobInfo on it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, it's just ETag, LastModified, and a boolean IsSealed.

ClientDiagnostics,
Pipeline,
Uri,
Version.ToVersionString(),
leaseId: conditions?.LeaseId,
ifModifiedSince: conditions?.IfModifiedSince,
ifUnmodifiedSince: conditions?.IfUnmodifiedSince,
ifMatch: conditions?.IfMatch,
ifNoneMatch: conditions?.IfNoneMatch,
async: async,
operationName: $"{nameof(AppendBlobClient)}.{nameof(Seal)}",
cancellationToken: cancellationToken)
.ConfigureAwait(false);

return Response.FromValue(
new BlobInfo
{
ETag = response.Value.ETag,
LastModified = response.Value.LastModified
},
response.GetRawResponse());
}
catch (Exception ex)
{
Pipeline.LogException(ex);
throw;
}
finally
{
Pipeline.LogMethodExit(nameof(AppendBlobClient));
}
}
}
#endregion Seal
}

/// <summary>
Expand Down
5 changes: 5 additions & 0 deletions sdk/storage/Azure.Storage.Blobs/src/Models/BlobInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ public partial class BlobDownloadDetails
#pragma warning disable CA1819 // Properties should not return arrays
public byte[] BlobContentHash => _flattened.BlobContentHash;
#pragma warning restore CA1819 // Properties should not return arrays

/// <summary>
/// If this blob is sealed.
/// </summary>
public bool BlobSealed => _flattened.BlobSealed;
}

/// <summary>
Expand Down
106 changes: 106 additions & 0 deletions sdk/storage/Azure.Storage.Blobs/tests/AppendBlobClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,112 @@ await TestHelper.AssertExpectedExceptionAsync<ArgumentNullException>(
}
}

[Test]
[ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2019_12_12)]
public async Task SealAsync()
{
// Arrange
await using DisposingContainer test = await GetTestContainerAsync();
AppendBlobClient appendBlob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName()));
await appendBlob.CreateAsync();

// Act
await appendBlob.SealAsync();
Response<BlobProperties> propertiesResponse = await appendBlob.GetPropertiesAsync();
Response<BlobDownloadInfo> downloadResponse = await appendBlob.DownloadAsync();

// Assert
Assert.IsTrue(propertiesResponse.Value.BlobSealed);
Assert.IsTrue(downloadResponse.Value.Details.BlobSealed);
}

[Test]
[ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2019_12_12)]
public async Task SealAsync_Error()
{
// Arrange
await using DisposingContainer test = await GetTestContainerAsync();
AppendBlobClient appendBlob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName()));

// Act
await TestHelper.AssertExpectedExceptionAsync<RequestFailedException>(
appendBlob.SealAsync(),
e => Assert.AreEqual(BlobErrorCode.BlobNotFound.ToString(), e.ErrorCode));
}

[Test]
[ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2019_12_12)]
public async Task SealAsync_AccessConditions()
{
var garbageLeaseId = GetGarbageLeaseId();
AccessConditionParameters[] testCases = new[]
{
new AccessConditionParameters(),
new AccessConditionParameters { IfModifiedSince = OldDate },
new AccessConditionParameters { IfUnmodifiedSince = NewDate },
new AccessConditionParameters { Match = ReceivedETag },
new AccessConditionParameters { NoneMatch = GarbageETag },
new AccessConditionParameters { LeaseId = ReceivedLeaseId },
};

foreach (AccessConditionParameters parameters in testCases)
{
await using DisposingContainer test = await GetTestContainerAsync();

// Arrange
AppendBlobClient blob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName()));
await blob.CreateAsync();
parameters.Match = await SetupBlobMatchCondition(blob, parameters.Match);
parameters.LeaseId = await SetupBlobLeaseCondition(blob, parameters.LeaseId, garbageLeaseId);
AppendBlobRequestConditions accessConditions = BuildDestinationAccessConditions(
parameters: parameters,
lease: true,
appendPosAndMaxSize: true);

// Act
Response<BlobInfo> response = await blob.SealAsync(accessConditions);

// Assert
Assert.IsNotNull(response.Value.ETag);
}
}

[Test]
[ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2019_12_12)]
public async Task SealAsync_AccessConditionsFailed()
{
var garbageLeaseId = GetGarbageLeaseId();
AccessConditionParameters[] testCases = new[]
{
new AccessConditionParameters { IfModifiedSince = NewDate },
new AccessConditionParameters { IfUnmodifiedSince = OldDate },
new AccessConditionParameters { Match = GarbageETag },
new AccessConditionParameters { NoneMatch = ReceivedETag },
new AccessConditionParameters { LeaseId = garbageLeaseId }
};

foreach (AccessConditionParameters parameters in testCases)
{
await using DisposingContainer test = await GetTestContainerAsync();

// Arrange
AppendBlobClient blob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName()));
// AppendBlob needs to exists for us to test CreateAsync() with access conditions
await blob.CreateAsync();
parameters.NoneMatch = await SetupBlobMatchCondition(blob, parameters.NoneMatch);
AppendBlobRequestConditions accessConditions = BuildDestinationAccessConditions(
parameters: parameters,
lease: true,
appendPosAndMaxSize: true);

// Act
await TestHelper.AssertExpectedExceptionAsync<RequestFailedException>(
blob.SealAsync(
conditions: accessConditions),
e => { });
}
}

private AppendBlobRequestConditions BuildDestinationAccessConditions(
AccessConditionParameters parameters,
bool lease = false,
Expand Down
Loading