Skip to content

Commit

Permalink
Add update blob properties support to cloud storage (#6640)
Browse files Browse the repository at this point in the history
  • Loading branch information
shishirx34 authored Nov 8, 2018
1 parent 556e915 commit 36acf28
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 72 deletions.
73 changes: 49 additions & 24 deletions src/NuGetGallery.Core/Services/CloudBlobCoreFileStorageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,17 +276,6 @@ await destBlob.StartCopyAsync(
throw new StorageException($"The blob copy operation had copy status {destBlob.CopyState.Status} ({destBlob.CopyState.StatusDescription}).");
}

var cacheControl = GetCacheControlForCopy(destFolderName);
if (!string.IsNullOrEmpty(cacheControl))
{
await destBlob.FetchAttributesAsync();
if (string.IsNullOrEmpty(destBlob.Properties.CacheControl))
{
destBlob.Properties.CacheControl = cacheControl;
await destBlob.SetPropertiesAsync();
}
}

return srcBlob.ETag;
}

Expand Down Expand Up @@ -448,6 +437,55 @@ public async Task SetMetadataAsync(
}
}

/// <summary>
/// Asynchronously sets blob properties.
/// </summary>
/// <param name="folderName">The folder (container) name.</param>
/// <param name="fileName">The blob file name.</param>
/// <param name="updatePropertiesAsync">A function which updates blob properties and returns <c>true</c>
/// for changes to be persisted or <c>false</c> for changes to be discarded.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task SetPropertiesAsync(
string folderName,
string fileName,
Func<Lazy<Task<Stream>>, BlobProperties, Task<bool>> updatePropertiesAsync)
{
if (folderName == null)
{
throw new ArgumentNullException(nameof(folderName));
}

if (fileName == null)
{
throw new ArgumentNullException(nameof(fileName));
}

if (updatePropertiesAsync == null)
{
throw new ArgumentNullException(nameof(updatePropertiesAsync));
}

var container = await GetContainerAsync(folderName);
var blob = container.GetBlobReference(fileName);

await blob.FetchAttributesAsync();

var lazyStream = new Lazy<Task<Stream>>(() => GetFileAsync(folderName, fileName));
var wasUpdated = await updatePropertiesAsync(lazyStream, blob.Properties);

if (wasUpdated)
{
var accessCondition = AccessConditionWrapper.GenerateIfMatchCondition(blob.ETag);
var mappedAccessCondition = new AccessCondition
{
IfNoneMatchETag = accessCondition.IfNoneMatchETag,
IfMatchETag = accessCondition.IfMatchETag
};

await blob.SetPropertiesAsync(mappedAccessCondition);
}
}

public async Task<string> GetETagOrNullAsync(
string folderName,
string fileName)
Expand Down Expand Up @@ -606,19 +644,6 @@ private static string GetContentType(string folderName)
}
}

private static string GetCacheControlForCopy(string folderName)
{
switch (folderName)
{
case CoreConstants.PackagesFolderName:
case CoreConstants.SymbolPackagesFolderName:
return CoreConstants.DefaultCacheControl;

default:
return null;
}
}

private static string GetCacheControl(string folderName)
{
switch (folderName)
Expand Down
5 changes: 5 additions & 0 deletions src/NuGetGallery.Core/Services/CloudBlobWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ public async Task SetPropertiesAsync()
await _blob.SetPropertiesAsync();
}

public async Task SetPropertiesAsync(AccessCondition accessCondition)
{
await _blob.SetPropertiesAsync(accessCondition, options: null, operationContext: null);
}

public async Task SetMetadataAsync(AccessCondition accessCondition)
{
await _blob.SetMetadataAsync(accessCondition, options: null, operationContext: null);
Expand Down
13 changes: 13 additions & 0 deletions src/NuGetGallery.Core/Services/ICoreFileStorageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.Storage.Blob;

namespace NuGetGallery
{
Expand Down Expand Up @@ -136,6 +137,18 @@ Task SetMetadataAsync(
string fileName,
Func<Lazy<Task<Stream>>, IDictionary<string, string>, Task<bool>> updateMetadataAsync);

/// <summary>
/// Updates properties on the file.
/// </summary>
/// <param name="folderName">The folder name.</param>
/// <param name="fileName">The file name.</param>
/// <param name="updatePropertiesAsync">A function that will update file properties.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task SetPropertiesAsync(
string folderName,
string fileName,
Func<Lazy<Task<Stream>>, BlobProperties, Task<bool>> updatePropertiesAsync);

/// <summary>
/// Returns the etag value for the specified blob. If the blob does not exists it will return null.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/NuGetGallery.Core/Services/ISimpleCloudBlob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public interface ISimpleCloudBlob

Task<bool> ExistsAsync();
Task SetPropertiesAsync();
Task SetPropertiesAsync(AccessCondition accessCondition);
Task SetMetadataAsync(AccessCondition accessCondition);
Task UploadFromStreamAsync(Stream source, bool overwrite);
Task UploadFromStreamAsync(Stream source, AccessCondition accessCondition);
Expand Down
9 changes: 9 additions & 0 deletions src/NuGetGallery/Services/FileSystemFileStorageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Threading.Tasks;
using System.Web.Hosting;
using System.Web.Mvc;
using Microsoft.WindowsAzure.Storage.Blob;
using NuGetGallery.Configuration;

namespace NuGetGallery
Expand Down Expand Up @@ -262,6 +263,14 @@ public Task SetMetadataAsync(
return Task.CompletedTask;
}

public Task SetPropertiesAsync(
string folderName,
string fileName,
Func<Lazy<Task<Stream>>, BlobProperties, Task<bool>> updatePropertiesAsync)
{
return Task.CompletedTask;
}

private static string BuildPath(string fileStorageDirectory, string folderName, string fileName)
{
// Resolve the file storage directory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1067,54 +1067,6 @@ await _target.CopyFileAsync(
Times.Once);
}

[Theory]
[InlineData(CoreConstants.PackagesFolderName)]
[InlineData(CoreConstants.SymbolPackagesFolderName)]
public async Task WillCopyAndSetCacheControlOnCopyForFolder(string folderName)
{
// Arrange
var instance = new TheCopyFileAsyncMethod();
instance._blobClient
.Setup(x => x.GetBlobFromUri(It.IsAny<Uri>()))
.Returns(instance._srcBlobMock.Object);
instance._blobClient
.Setup(x => x.GetContainerReference(folderName))
.Returns(() => instance._destContainer.Object);

instance._destBlobMock
.Setup(x => x.StartCopyAsync(It.IsAny<ISimpleCloudBlob>(), It.IsAny<AccessCondition>(), It.IsAny<AccessCondition>()))
.Returns(Task.FromResult(0))
.Callback<ISimpleCloudBlob, AccessCondition, AccessCondition>((_, __, ___) =>
{
SetDestCopyStatus(CopyStatus.Success);
});

// Act
await instance._target.CopyFileAsync(
instance._srcUri,
folderName,
instance._destFileName,
AccessConditionWrapper.GenerateIfNotExistsCondition());

// Assert
instance._destBlobMock.Verify(
x => x.StartCopyAsync(instance._srcBlobMock.Object, It.IsAny<AccessCondition>(), It.IsAny<AccessCondition>()),
Times.Once);
instance._destBlobMock.Verify(
x => x.StartCopyAsync(It.IsAny<ISimpleCloudBlob>(), It.IsAny<AccessCondition>(), It.IsAny<AccessCondition>()),
Times.Once);
instance._destBlobMock.Verify(
x => x.SetPropertiesAsync(),
Times.Once);
instance._destBlobMock.Verify(
x => x.StartCopyAsync(It.IsAny<ISimpleCloudBlob>(), It.IsAny<AccessCondition>(), It.IsAny<AccessCondition>()),
Times.Once);
Assert.NotNull(instance._destProperties.CacheControl);
instance._blobClient.Verify(
x => x.GetBlobFromUri(instance._srcUri),
Times.Once);
}

[Fact]
public async Task WillCopyTheFileIfDestinationDoesNotExist()
{
Expand Down Expand Up @@ -1461,6 +1413,110 @@ await _service.SetMetadataAsync(
}
}

public class TheSetPropertiesAsyncMethod
{
private const string _content = "peach";

private readonly Mock<ICloudBlobClient> _blobClient;
private readonly Mock<ICloudBlobContainer> _blobContainer;
private readonly Mock<ISimpleCloudBlob> _blob;
private readonly CloudBlobCoreFileStorageService _service;

public TheSetPropertiesAsyncMethod()
{
_blobClient = new Mock<ICloudBlobClient>();
_blobContainer = new Mock<ICloudBlobContainer>();
_blob = new Mock<ISimpleCloudBlob>();

_blobClient.Setup(x => x.GetContainerReference(It.IsAny<string>()))
.Returns(_blobContainer.Object);
_blobContainer.Setup(x => x.CreateIfNotExistAsync())
.Returns(Task.FromResult(0));
_blobContainer.Setup(x => x.SetPermissionsAsync(It.IsAny<BlobContainerPermissions>()))
.Returns(Task.FromResult(0));
_blobContainer.Setup(x => x.GetBlobReference(It.IsAny<string>()))
.Returns(_blob.Object);

_service = CreateService(fakeBlobClient: _blobClient);
}

[Fact]
public async Task WhenLazyStreamRead_ReturnsContent()
{
_blob.Setup(x => x.DownloadToStreamAsync(It.IsAny<Stream>(), It.IsAny<AccessCondition>()))
.Callback<Stream, AccessCondition>((stream, _) =>
{
using (var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: 4096, leaveOpen: true))
{
writer.Write(_content);
}
})
.Returns(Task.FromResult(0));

await _service.SetPropertiesAsync(
folderName: CoreConstants.PackagesFolderName,
fileName: "a",
updatePropertiesAsync: async (lazyStream, properties) =>
{
using (var stream = await lazyStream.Value)
using (var reader = new StreamReader(stream))
{
Assert.Equal(_content, reader.ReadToEnd());
}
return false;
});

_blob.VerifyAll();
_blobContainer.VerifyAll();
_blobClient.VerifyAll();
}

[Fact]
public async Task WhenReturnValueIsFalse_PropertyChangesAreNotPersisted()
{
_blob.SetupGet(x => x.Properties)
.Returns(new BlobProperties());

await _service.SetPropertiesAsync(
folderName: CoreConstants.PackagesFolderName,
fileName: "a",
updatePropertiesAsync: (lazyStream, properties) =>
{
Assert.NotNull(properties);
return Task.FromResult(false);
});

_blob.VerifyAll();
_blobContainer.VerifyAll();
_blobClient.VerifyAll();
}

[Fact]
public async Task WhenReturnValueIsTrue_PropertiesChangesArePersisted()
{
_blob.SetupGet(x => x.Properties)
.Returns(new BlobProperties());
_blob.Setup(x => x.SetPropertiesAsync(It.IsNotNull<AccessCondition>()))
.Returns(Task.FromResult(0));

await _service.SetPropertiesAsync(
folderName: CoreConstants.PackagesFolderName,
fileName: "a",
updatePropertiesAsync: (lazyStream, properties) =>
{
Assert.NotNull(properties);
return Task.FromResult(true);
});

_blob.VerifyAll();
_blobContainer.VerifyAll();
_blobClient.VerifyAll();
}
}

public class TheGetETagMethod
{
private const string _etag = "dummy_etag";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -733,5 +733,16 @@ public async Task NoOps()
await service.SetMetadataAsync(folderName: null, fileName: null, updateMetadataAsync: null);
}
}

public class TheSetPropertiesAsyncMethod
{
[Fact]
public async Task NoOps()
{
var service = CreateService();

await service.SetPropertiesAsync(folderName: null, fileName: null, updatePropertiesAsync: null);
}
}
}
}

0 comments on commit 36acf28

Please sign in to comment.