Skip to content

Commit 36acf28

Browse files
authored
Add update blob properties support to cloud storage (#6640)
1 parent 556e915 commit 36acf28

7 files changed

Lines changed: 192 additions & 72 deletions

File tree

src/NuGetGallery.Core/Services/CloudBlobCoreFileStorageService.cs

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -276,17 +276,6 @@ await destBlob.StartCopyAsync(
276276
throw new StorageException($"The blob copy operation had copy status {destBlob.CopyState.Status} ({destBlob.CopyState.StatusDescription}).");
277277
}
278278

279-
var cacheControl = GetCacheControlForCopy(destFolderName);
280-
if (!string.IsNullOrEmpty(cacheControl))
281-
{
282-
await destBlob.FetchAttributesAsync();
283-
if (string.IsNullOrEmpty(destBlob.Properties.CacheControl))
284-
{
285-
destBlob.Properties.CacheControl = cacheControl;
286-
await destBlob.SetPropertiesAsync();
287-
}
288-
}
289-
290279
return srcBlob.ETag;
291280
}
292281

@@ -448,6 +437,55 @@ public async Task SetMetadataAsync(
448437
}
449438
}
450439

440+
/// <summary>
441+
/// Asynchronously sets blob properties.
442+
/// </summary>
443+
/// <param name="folderName">The folder (container) name.</param>
444+
/// <param name="fileName">The blob file name.</param>
445+
/// <param name="updatePropertiesAsync">A function which updates blob properties and returns <c>true</c>
446+
/// for changes to be persisted or <c>false</c> for changes to be discarded.</param>
447+
/// <returns>A task that represents the asynchronous operation.</returns>
448+
public async Task SetPropertiesAsync(
449+
string folderName,
450+
string fileName,
451+
Func<Lazy<Task<Stream>>, BlobProperties, Task<bool>> updatePropertiesAsync)
452+
{
453+
if (folderName == null)
454+
{
455+
throw new ArgumentNullException(nameof(folderName));
456+
}
457+
458+
if (fileName == null)
459+
{
460+
throw new ArgumentNullException(nameof(fileName));
461+
}
462+
463+
if (updatePropertiesAsync == null)
464+
{
465+
throw new ArgumentNullException(nameof(updatePropertiesAsync));
466+
}
467+
468+
var container = await GetContainerAsync(folderName);
469+
var blob = container.GetBlobReference(fileName);
470+
471+
await blob.FetchAttributesAsync();
472+
473+
var lazyStream = new Lazy<Task<Stream>>(() => GetFileAsync(folderName, fileName));
474+
var wasUpdated = await updatePropertiesAsync(lazyStream, blob.Properties);
475+
476+
if (wasUpdated)
477+
{
478+
var accessCondition = AccessConditionWrapper.GenerateIfMatchCondition(blob.ETag);
479+
var mappedAccessCondition = new AccessCondition
480+
{
481+
IfNoneMatchETag = accessCondition.IfNoneMatchETag,
482+
IfMatchETag = accessCondition.IfMatchETag
483+
};
484+
485+
await blob.SetPropertiesAsync(mappedAccessCondition);
486+
}
487+
}
488+
451489
public async Task<string> GetETagOrNullAsync(
452490
string folderName,
453491
string fileName)
@@ -606,19 +644,6 @@ private static string GetContentType(string folderName)
606644
}
607645
}
608646

609-
private static string GetCacheControlForCopy(string folderName)
610-
{
611-
switch (folderName)
612-
{
613-
case CoreConstants.PackagesFolderName:
614-
case CoreConstants.SymbolPackagesFolderName:
615-
return CoreConstants.DefaultCacheControl;
616-
617-
default:
618-
return null;
619-
}
620-
}
621-
622647
private static string GetCacheControl(string folderName)
623648
{
624649
switch (folderName)

src/NuGetGallery.Core/Services/CloudBlobWrapper.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ public async Task SetPropertiesAsync()
6666
await _blob.SetPropertiesAsync();
6767
}
6868

69+
public async Task SetPropertiesAsync(AccessCondition accessCondition)
70+
{
71+
await _blob.SetPropertiesAsync(accessCondition, options: null, operationContext: null);
72+
}
73+
6974
public async Task SetMetadataAsync(AccessCondition accessCondition)
7075
{
7176
await _blob.SetMetadataAsync(accessCondition, options: null, operationContext: null);

src/NuGetGallery.Core/Services/ICoreFileStorageService.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.IO;
77
using System.Threading.Tasks;
8+
using Microsoft.WindowsAzure.Storage.Blob;
89

910
namespace NuGetGallery
1011
{
@@ -136,6 +137,18 @@ Task SetMetadataAsync(
136137
string fileName,
137138
Func<Lazy<Task<Stream>>, IDictionary<string, string>, Task<bool>> updateMetadataAsync);
138139

140+
/// <summary>
141+
/// Updates properties on the file.
142+
/// </summary>
143+
/// <param name="folderName">The folder name.</param>
144+
/// <param name="fileName">The file name.</param>
145+
/// <param name="updatePropertiesAsync">A function that will update file properties.</param>
146+
/// <returns>A task that represents the asynchronous operation.</returns>
147+
Task SetPropertiesAsync(
148+
string folderName,
149+
string fileName,
150+
Func<Lazy<Task<Stream>>, BlobProperties, Task<bool>> updatePropertiesAsync);
151+
139152
/// <summary>
140153
/// Returns the etag value for the specified blob. If the blob does not exists it will return null.
141154
/// </summary>

src/NuGetGallery.Core/Services/ISimpleCloudBlob.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public interface ISimpleCloudBlob
2626

2727
Task<bool> ExistsAsync();
2828
Task SetPropertiesAsync();
29+
Task SetPropertiesAsync(AccessCondition accessCondition);
2930
Task SetMetadataAsync(AccessCondition accessCondition);
3031
Task UploadFromStreamAsync(Stream source, bool overwrite);
3132
Task UploadFromStreamAsync(Stream source, AccessCondition accessCondition);

src/NuGetGallery/Services/FileSystemFileStorageService.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Threading.Tasks;
99
using System.Web.Hosting;
1010
using System.Web.Mvc;
11+
using Microsoft.WindowsAzure.Storage.Blob;
1112
using NuGetGallery.Configuration;
1213

1314
namespace NuGetGallery
@@ -262,6 +263,14 @@ public Task SetMetadataAsync(
262263
return Task.CompletedTask;
263264
}
264265

266+
public Task SetPropertiesAsync(
267+
string folderName,
268+
string fileName,
269+
Func<Lazy<Task<Stream>>, BlobProperties, Task<bool>> updatePropertiesAsync)
270+
{
271+
return Task.CompletedTask;
272+
}
273+
265274
private static string BuildPath(string fileStorageDirectory, string folderName, string fileName)
266275
{
267276
// Resolve the file storage directory

tests/NuGetGallery.Core.Facts/Services/CloudBlobCoreFileStorageServiceFacts.cs

Lines changed: 104 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,54 +1067,6 @@ await _target.CopyFileAsync(
10671067
Times.Once);
10681068
}
10691069

1070-
[Theory]
1071-
[InlineData(CoreConstants.PackagesFolderName)]
1072-
[InlineData(CoreConstants.SymbolPackagesFolderName)]
1073-
public async Task WillCopyAndSetCacheControlOnCopyForFolder(string folderName)
1074-
{
1075-
// Arrange
1076-
var instance = new TheCopyFileAsyncMethod();
1077-
instance._blobClient
1078-
.Setup(x => x.GetBlobFromUri(It.IsAny<Uri>()))
1079-
.Returns(instance._srcBlobMock.Object);
1080-
instance._blobClient
1081-
.Setup(x => x.GetContainerReference(folderName))
1082-
.Returns(() => instance._destContainer.Object);
1083-
1084-
instance._destBlobMock
1085-
.Setup(x => x.StartCopyAsync(It.IsAny<ISimpleCloudBlob>(), It.IsAny<AccessCondition>(), It.IsAny<AccessCondition>()))
1086-
.Returns(Task.FromResult(0))
1087-
.Callback<ISimpleCloudBlob, AccessCondition, AccessCondition>((_, __, ___) =>
1088-
{
1089-
SetDestCopyStatus(CopyStatus.Success);
1090-
});
1091-
1092-
// Act
1093-
await instance._target.CopyFileAsync(
1094-
instance._srcUri,
1095-
folderName,
1096-
instance._destFileName,
1097-
AccessConditionWrapper.GenerateIfNotExistsCondition());
1098-
1099-
// Assert
1100-
instance._destBlobMock.Verify(
1101-
x => x.StartCopyAsync(instance._srcBlobMock.Object, It.IsAny<AccessCondition>(), It.IsAny<AccessCondition>()),
1102-
Times.Once);
1103-
instance._destBlobMock.Verify(
1104-
x => x.StartCopyAsync(It.IsAny<ISimpleCloudBlob>(), It.IsAny<AccessCondition>(), It.IsAny<AccessCondition>()),
1105-
Times.Once);
1106-
instance._destBlobMock.Verify(
1107-
x => x.SetPropertiesAsync(),
1108-
Times.Once);
1109-
instance._destBlobMock.Verify(
1110-
x => x.StartCopyAsync(It.IsAny<ISimpleCloudBlob>(), It.IsAny<AccessCondition>(), It.IsAny<AccessCondition>()),
1111-
Times.Once);
1112-
Assert.NotNull(instance._destProperties.CacheControl);
1113-
instance._blobClient.Verify(
1114-
x => x.GetBlobFromUri(instance._srcUri),
1115-
Times.Once);
1116-
}
1117-
11181070
[Fact]
11191071
public async Task WillCopyTheFileIfDestinationDoesNotExist()
11201072
{
@@ -1461,6 +1413,110 @@ await _service.SetMetadataAsync(
14611413
}
14621414
}
14631415

1416+
public class TheSetPropertiesAsyncMethod
1417+
{
1418+
private const string _content = "peach";
1419+
1420+
private readonly Mock<ICloudBlobClient> _blobClient;
1421+
private readonly Mock<ICloudBlobContainer> _blobContainer;
1422+
private readonly Mock<ISimpleCloudBlob> _blob;
1423+
private readonly CloudBlobCoreFileStorageService _service;
1424+
1425+
public TheSetPropertiesAsyncMethod()
1426+
{
1427+
_blobClient = new Mock<ICloudBlobClient>();
1428+
_blobContainer = new Mock<ICloudBlobContainer>();
1429+
_blob = new Mock<ISimpleCloudBlob>();
1430+
1431+
_blobClient.Setup(x => x.GetContainerReference(It.IsAny<string>()))
1432+
.Returns(_blobContainer.Object);
1433+
_blobContainer.Setup(x => x.CreateIfNotExistAsync())
1434+
.Returns(Task.FromResult(0));
1435+
_blobContainer.Setup(x => x.SetPermissionsAsync(It.IsAny<BlobContainerPermissions>()))
1436+
.Returns(Task.FromResult(0));
1437+
_blobContainer.Setup(x => x.GetBlobReference(It.IsAny<string>()))
1438+
.Returns(_blob.Object);
1439+
1440+
_service = CreateService(fakeBlobClient: _blobClient);
1441+
}
1442+
1443+
[Fact]
1444+
public async Task WhenLazyStreamRead_ReturnsContent()
1445+
{
1446+
_blob.Setup(x => x.DownloadToStreamAsync(It.IsAny<Stream>(), It.IsAny<AccessCondition>()))
1447+
.Callback<Stream, AccessCondition>((stream, _) =>
1448+
{
1449+
using (var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: 4096, leaveOpen: true))
1450+
{
1451+
writer.Write(_content);
1452+
}
1453+
})
1454+
.Returns(Task.FromResult(0));
1455+
1456+
await _service.SetPropertiesAsync(
1457+
folderName: CoreConstants.PackagesFolderName,
1458+
fileName: "a",
1459+
updatePropertiesAsync: async (lazyStream, properties) =>
1460+
{
1461+
using (var stream = await lazyStream.Value)
1462+
using (var reader = new StreamReader(stream))
1463+
{
1464+
Assert.Equal(_content, reader.ReadToEnd());
1465+
}
1466+
1467+
return false;
1468+
});
1469+
1470+
_blob.VerifyAll();
1471+
_blobContainer.VerifyAll();
1472+
_blobClient.VerifyAll();
1473+
}
1474+
1475+
[Fact]
1476+
public async Task WhenReturnValueIsFalse_PropertyChangesAreNotPersisted()
1477+
{
1478+
_blob.SetupGet(x => x.Properties)
1479+
.Returns(new BlobProperties());
1480+
1481+
await _service.SetPropertiesAsync(
1482+
folderName: CoreConstants.PackagesFolderName,
1483+
fileName: "a",
1484+
updatePropertiesAsync: (lazyStream, properties) =>
1485+
{
1486+
Assert.NotNull(properties);
1487+
1488+
return Task.FromResult(false);
1489+
});
1490+
1491+
_blob.VerifyAll();
1492+
_blobContainer.VerifyAll();
1493+
_blobClient.VerifyAll();
1494+
}
1495+
1496+
[Fact]
1497+
public async Task WhenReturnValueIsTrue_PropertiesChangesArePersisted()
1498+
{
1499+
_blob.SetupGet(x => x.Properties)
1500+
.Returns(new BlobProperties());
1501+
_blob.Setup(x => x.SetPropertiesAsync(It.IsNotNull<AccessCondition>()))
1502+
.Returns(Task.FromResult(0));
1503+
1504+
await _service.SetPropertiesAsync(
1505+
folderName: CoreConstants.PackagesFolderName,
1506+
fileName: "a",
1507+
updatePropertiesAsync: (lazyStream, properties) =>
1508+
{
1509+
Assert.NotNull(properties);
1510+
1511+
return Task.FromResult(true);
1512+
});
1513+
1514+
_blob.VerifyAll();
1515+
_blobContainer.VerifyAll();
1516+
_blobClient.VerifyAll();
1517+
}
1518+
}
1519+
14641520
public class TheGetETagMethod
14651521
{
14661522
private const string _etag = "dummy_etag";

tests/NuGetGallery.Facts/Services/FileSystemFileStorageServiceFacts.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,5 +733,16 @@ public async Task NoOps()
733733
await service.SetMetadataAsync(folderName: null, fileName: null, updateMetadataAsync: null);
734734
}
735735
}
736+
737+
public class TheSetPropertiesAsyncMethod
738+
{
739+
[Fact]
740+
public async Task NoOps()
741+
{
742+
var service = CreateService();
743+
744+
await service.SetPropertiesAsync(folderName: null, fileName: null, updatePropertiesAsync: null);
745+
}
746+
}
736747
}
737748
}

0 commit comments

Comments
 (0)