Port DataProtection blob XmlRepository (#163)
This commit is contained in:
parent
8f8152f910
commit
0e210dadea
|
|
@ -34,12 +34,24 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataPr
|
||||||
EndProject
|
EndProject
|
||||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataProtection.Redis", "src\Microsoft.AspNetCore.DataProtection.Redis\Microsoft.AspNetCore.DataProtection.Redis.xproj", "{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}"
|
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataProtection.Redis", "src\Microsoft.AspNetCore.DataProtection.Redis\Microsoft.AspNetCore.DataProtection.Redis.xproj", "{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Redis", "samples\Redis\Redis.xproj", "{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}"
|
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataProtection.Azure.Blob", "src\Microsoft.AspNetCore.DataProtection.Azure.Blob\Microsoft.AspNetCore.DataProtection.Azure.Blob.xproj", "{CC799B57-81E2-4F45-8A32-0D5F49753C3F}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{3A6C77DB-FD3D-4B20-A52B-34F7A7E1AED2}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5A3A5DE3-49AD-431C-971D-B01B62D94AE2}"
|
||||||
|
EndProject
|
||||||
|
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "AzureBlob", "samples\AzureBlob\AzureBlob.xproj", "{B07435B3-CD81-4E3B-88A5-6384821E1C01}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataProtection.Redis.Test", "test\Microsoft.AspNetCore.DataProtection.Redis.Test\Microsoft.AspNetCore.DataProtection.Redis.Test.xproj", "{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}"
|
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataProtection.Redis.Test", "test\Microsoft.AspNetCore.DataProtection.Redis.Test\Microsoft.AspNetCore.DataProtection.Redis.Test.xproj", "{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E1D86B1B-41D8-43C9-97FD-C2BF65C414E2}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
global.json = global.json
|
||||||
|
NuGet.config = NuGet.config
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
|
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataProtection.Azure.Blob.Test", "test\Microsoft.AspNetCore.DataProtection.Azure.Blob.Test\Microsoft.AspNetCore.DataProtection.Azure.Blob.Test.xproj", "{8C41240E-48F8-402F-9388-74CFE27F4D76}"
|
||||||
|
EndProject
|
||||||
|
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Redis", "samples\Redis\Redis.xproj", "{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -156,14 +168,22 @@ Global
|
||||||
{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Release|Any CPU.Build.0 = Release|Any CPU
|
{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Release|x86.ActiveCfg = Release|Any CPU
|
{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Release|x86.Build.0 = Release|Any CPU
|
{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|x86.Build.0 = Debug|Any CPU
|
{CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|Any CPU.Build.0 = Release|Any CPU
|
{CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|x86.ActiveCfg = Release|Any CPU
|
{CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|x86.Build.0 = Release|Any CPU
|
{CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{B07435B3-CD81-4E3B-88A5-6384821E1C01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B07435B3-CD81-4E3B-88A5-6384821E1C01}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B07435B3-CD81-4E3B-88A5-6384821E1C01}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{B07435B3-CD81-4E3B-88A5-6384821E1C01}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{B07435B3-CD81-4E3B-88A5-6384821E1C01}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B07435B3-CD81-4E3B-88A5-6384821E1C01}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B07435B3-CD81-4E3B-88A5-6384821E1C01}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{B07435B3-CD81-4E3B-88A5-6384821E1C01}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
|
@ -172,6 +192,22 @@ Global
|
||||||
{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Release|x86.ActiveCfg = Release|Any CPU
|
{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Release|x86.Build.0 = Release|Any CPU
|
{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{8C41240E-48F8-402F-9388-74CFE27F4D76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{8C41240E-48F8-402F-9388-74CFE27F4D76}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{8C41240E-48F8-402F-9388-74CFE27F4D76}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{8C41240E-48F8-402F-9388-74CFE27F4D76}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{8C41240E-48F8-402F-9388-74CFE27F4D76}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{8C41240E-48F8-402F-9388-74CFE27F4D76}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{8C41240E-48F8-402F-9388-74CFE27F4D76}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{8C41240E-48F8-402F-9388-74CFE27F4D76}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -191,7 +227,10 @@ Global
|
||||||
{04AA8E60-A053-4D50-89FE-E76C3DF45200} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
|
{04AA8E60-A053-4D50-89FE-E76C3DF45200} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
|
||||||
{BF8681DB-C28B-441F-BD92-0DCFE9537A9F} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
|
{BF8681DB-C28B-441F-BD92-0DCFE9537A9F} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
|
||||||
{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
|
{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
|
||||||
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9} = {3A6C77DB-FD3D-4B20-A52B-34F7A7E1AED2}
|
{CC799B57-81E2-4F45-8A32-0D5F49753C3F} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
|
||||||
|
{B07435B3-CD81-4E3B-88A5-6384821E1C01} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2}
|
||||||
{ABCF00E5-5B2F-469C-90DC-908C5A04C08D} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
|
{ABCF00E5-5B2F-469C-90DC-908C5A04C08D} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
|
||||||
|
{8C41240E-48F8-402F-9388-74CFE27F4D76} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
|
||||||
|
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
"Microsoft.AspNetCore.Cryptography.KeyDerivation": { },
|
"Microsoft.AspNetCore.Cryptography.KeyDerivation": { },
|
||||||
"Microsoft.AspNetCore.DataProtection": { },
|
"Microsoft.AspNetCore.DataProtection": { },
|
||||||
"Microsoft.AspNetCore.DataProtection.Abstractions": { },
|
"Microsoft.AspNetCore.DataProtection.Abstractions": { },
|
||||||
|
"Microsoft.AspNetCore.DataProtection.Azure.Blob": { },
|
||||||
"Microsoft.AspNetCore.DataProtection.Extensions": { },
|
"Microsoft.AspNetCore.DataProtection.Extensions": { },
|
||||||
"Microsoft.AspNetCore.DataProtection.Redis": { },
|
"Microsoft.AspNetCore.DataProtection.Redis": { },
|
||||||
"Microsoft.AspNetCore.DataProtection.SystemWeb": { }
|
"Microsoft.AspNetCore.DataProtection.SystemWeb": { }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||||
|
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||||
|
<PropertyGroup Label="Globals">
|
||||||
|
<ProjectGuid>b07435b3-cd81-4e3b-88a5-6384821e1c01</ProjectGuid>
|
||||||
|
<RootNamespace>AzureBlob</RootNamespace>
|
||||||
|
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||||
|
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||||
|
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.WindowsAzure.Storage;
|
||||||
|
using Microsoft.AspNetCore.DataProtection.Azure.Blob;
|
||||||
|
|
||||||
|
namespace AzureBlob
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
var storageAccount = CloudStorageAccount.DevelopmentStorageAccount;
|
||||||
|
var client = storageAccount.CreateCloudBlobClient();
|
||||||
|
var container = client.GetContainerReference("key-container");
|
||||||
|
|
||||||
|
// The container must exist before calling the DataProtection APIs.
|
||||||
|
// The specific file within the container does not have to exist,
|
||||||
|
// as it will be created on-demand.
|
||||||
|
|
||||||
|
container.CreateIfNotExistsAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
// Configure
|
||||||
|
|
||||||
|
var serviceCollection = new ServiceCollection();
|
||||||
|
serviceCollection.AddLogging();
|
||||||
|
serviceCollection.AddDataProtection()
|
||||||
|
.PersistKeysToAzureBlobStorage(container, "keys.xml");
|
||||||
|
|
||||||
|
var services = serviceCollection.BuildServiceProvider();
|
||||||
|
var loggerFactory = services.GetService<ILoggerFactory>();
|
||||||
|
loggerFactory.AddConsole(Microsoft.Extensions.Logging.LogLevel.Trace);
|
||||||
|
|
||||||
|
// Run a sample payload
|
||||||
|
|
||||||
|
var protector = services.GetDataProtector("sample-purpose");
|
||||||
|
var protectedData = protector.Protect("Hello world!");
|
||||||
|
Console.WriteLine(protectedData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"version": "1.0.0-*",
|
||||||
|
"buildOptions": {
|
||||||
|
"emitEntryPoint": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.AspNetCore.DataProtection": "1.1.0-*",
|
||||||
|
"Microsoft.AspNetCore.DataProtection.Azure.Blob": "1.1.0-*",
|
||||||
|
"Microsoft.Extensions.DependencyInjection": "1.1.0-*",
|
||||||
|
"Microsoft.Extensions.Logging": "1.1.0-*",
|
||||||
|
"Microsoft.Extensions.Logging.Console": "1.1.0-*",
|
||||||
|
"Microsoft.NETCore.App": {
|
||||||
|
"type": "platform",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"frameworks": {
|
||||||
|
"netcoreapp1.0": {
|
||||||
|
"imports": [
|
||||||
|
"portable-net45+win8+wp8+wpa81"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,295 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.ExceptionServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using Microsoft.AspNetCore.DataProtection.Repositories;
|
||||||
|
using Microsoft.WindowsAzure.Storage;
|
||||||
|
using Microsoft.WindowsAzure.Storage.Blob;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.DataProtection.Azure.Blob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An <see cref="IXmlRepository"/> which is backed by Azure Blob Storage.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Instances of this type are thread-safe.
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class AzureBlobXmlRepository : IXmlRepository
|
||||||
|
{
|
||||||
|
private const int ConflictMaxRetries = 5;
|
||||||
|
private static readonly TimeSpan ConflictBackoffPeriod = TimeSpan.FromMilliseconds(200);
|
||||||
|
|
||||||
|
private static readonly XName RepositoryElementName = "repository";
|
||||||
|
|
||||||
|
private readonly Func<ICloudBlob> _blobRefFactory;
|
||||||
|
private readonly Random _random;
|
||||||
|
private BlobData _cachedBlobData;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the <see cref="AzureBlobXmlRepository"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="blobRefFactory">A factory which can create <see cref="ICloudBlob"/>
|
||||||
|
/// instances. The factory must be thread-safe for invocation by multiple
|
||||||
|
/// concurrent threads, and each invocation must return a new object.</param>
|
||||||
|
public AzureBlobXmlRepository(Func<ICloudBlob> blobRefFactory)
|
||||||
|
{
|
||||||
|
if (blobRefFactory == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(blobRefFactory));
|
||||||
|
}
|
||||||
|
|
||||||
|
_blobRefFactory = blobRefFactory;
|
||||||
|
_random = new Random();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<XElement> GetAllElements()
|
||||||
|
{
|
||||||
|
var blobRef = CreateFreshBlobRef();
|
||||||
|
|
||||||
|
// Shunt the work onto a ThreadPool thread so that it's independent of any
|
||||||
|
// existing sync context or other potentially deadlock-causing items.
|
||||||
|
|
||||||
|
var elements = Task.Run(() => GetAllElementsAsync(blobRef)).GetAwaiter().GetResult();
|
||||||
|
return new ReadOnlyCollection<XElement>(elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StoreElement(XElement element, string friendlyName)
|
||||||
|
{
|
||||||
|
if (element == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(element));
|
||||||
|
}
|
||||||
|
|
||||||
|
var blobRef = CreateFreshBlobRef();
|
||||||
|
|
||||||
|
// Shunt the work onto a ThreadPool thread so that it's independent of any
|
||||||
|
// existing sync context or other potentially deadlock-causing items.
|
||||||
|
|
||||||
|
Task.Run(() => StoreElementAsync(blobRef, element)).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private XDocument CreateDocumentFromBlob(byte[] blob)
|
||||||
|
{
|
||||||
|
using (var memoryStream = new MemoryStream(blob))
|
||||||
|
{
|
||||||
|
var xmlReaderSettings = new XmlReaderSettings()
|
||||||
|
{
|
||||||
|
DtdProcessing = DtdProcessing.Prohibit, IgnoreProcessingInstructions = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using (var xmlReader = XmlReader.Create(memoryStream, xmlReaderSettings))
|
||||||
|
{
|
||||||
|
return XDocument.Load(xmlReader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ICloudBlob CreateFreshBlobRef()
|
||||||
|
{
|
||||||
|
// ICloudBlob instances aren't thread-safe, so we need to make sure we're working
|
||||||
|
// with a fresh instance that won't be mutated by another thread.
|
||||||
|
|
||||||
|
var blobRef = _blobRefFactory();
|
||||||
|
if (blobRef == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("The ICloudBlob factory method returned null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return blobRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IList<XElement>> GetAllElementsAsync(ICloudBlob blobRef)
|
||||||
|
{
|
||||||
|
var data = await GetLatestDataAsync(blobRef);
|
||||||
|
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
// no data in blob storage
|
||||||
|
return new XElement[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The document will look like this:
|
||||||
|
//
|
||||||
|
// <root>
|
||||||
|
// <child />
|
||||||
|
// <child />
|
||||||
|
// ...
|
||||||
|
// </root>
|
||||||
|
//
|
||||||
|
// We want to return the first-level child elements to our caller.
|
||||||
|
|
||||||
|
var doc = CreateDocumentFromBlob(data.BlobContents);
|
||||||
|
return doc.Root.Elements().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<BlobData> GetLatestDataAsync(ICloudBlob blobRef)
|
||||||
|
{
|
||||||
|
// Set the appropriate AccessCondition based on what we believe the latest
|
||||||
|
// file contents to be, then make the request.
|
||||||
|
|
||||||
|
var latestCachedData = Volatile.Read(ref _cachedBlobData); // local ref so field isn't mutated under our feet
|
||||||
|
var accessCondition = (latestCachedData != null)
|
||||||
|
? AccessCondition.GenerateIfNoneMatchCondition(latestCachedData.ETag)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var memoryStream = new MemoryStream())
|
||||||
|
{
|
||||||
|
await blobRef.DownloadToStreamAsync(
|
||||||
|
target: memoryStream,
|
||||||
|
accessCondition: accessCondition,
|
||||||
|
options: null,
|
||||||
|
operationContext: null);
|
||||||
|
|
||||||
|
// At this point, our original cache either didn't exist or was outdated.
|
||||||
|
// We'll update it now and return the updated value;
|
||||||
|
|
||||||
|
latestCachedData = new BlobData()
|
||||||
|
{
|
||||||
|
BlobContents = memoryStream.ToArray(),
|
||||||
|
ETag = blobRef.Properties.ETag
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
Volatile.Write(ref _cachedBlobData, latestCachedData);
|
||||||
|
}
|
||||||
|
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 304)
|
||||||
|
{
|
||||||
|
// 304 Not Modified
|
||||||
|
// Thrown when we already have the latest cached data.
|
||||||
|
// This isn't an error; we'll return our cached copy of the data.
|
||||||
|
}
|
||||||
|
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404)
|
||||||
|
{
|
||||||
|
// 404 Not Found
|
||||||
|
// Thrown when no file exists in storage.
|
||||||
|
// This isn't an error; we'll delete our cached copy of data.
|
||||||
|
|
||||||
|
latestCachedData = null;
|
||||||
|
Volatile.Write(ref _cachedBlobData, latestCachedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return latestCachedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetRandomizedBackoffPeriod()
|
||||||
|
{
|
||||||
|
// returns a TimeSpan in the range [0.8, 1.0) * ConflictBackoffPeriod
|
||||||
|
// not used for crypto purposes
|
||||||
|
var multiplier = 0.8 + (_random.NextDouble() * 0.2);
|
||||||
|
return (int) (multiplier * ConflictBackoffPeriod.Ticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StoreElementAsync(ICloudBlob blobRef, XElement element)
|
||||||
|
{
|
||||||
|
// holds the last error in case we need to rethrow it
|
||||||
|
ExceptionDispatchInfo lastError = null;
|
||||||
|
|
||||||
|
for (var i = 0; i < ConflictMaxRetries; i++)
|
||||||
|
{
|
||||||
|
if (i > 1)
|
||||||
|
{
|
||||||
|
// If multiple conflicts occurred, wait a small period of time before retrying
|
||||||
|
// the operation so that other writers can make forward progress.
|
||||||
|
await Task.Delay(GetRandomizedBackoffPeriod());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
// If at least one conflict occurred, make sure we have an up-to-date
|
||||||
|
// view of the blob contents.
|
||||||
|
await GetLatestDataAsync(blobRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge the new element into the document. If no document exists,
|
||||||
|
// create a new default document and inject this element into it.
|
||||||
|
|
||||||
|
var latestData = Volatile.Read(ref _cachedBlobData);
|
||||||
|
var doc = (latestData != null)
|
||||||
|
? CreateDocumentFromBlob(latestData.BlobContents)
|
||||||
|
: new XDocument(new XElement(RepositoryElementName));
|
||||||
|
doc.Root.Add(element);
|
||||||
|
|
||||||
|
// Turn this document back into a byte[].
|
||||||
|
|
||||||
|
var serializedDoc = new MemoryStream();
|
||||||
|
doc.Save(serializedDoc, SaveOptions.DisableFormatting);
|
||||||
|
|
||||||
|
// Generate the appropriate precondition header based on whether or not
|
||||||
|
// we believe data already exists in storage.
|
||||||
|
|
||||||
|
AccessCondition accessCondition;
|
||||||
|
if (latestData != null)
|
||||||
|
{
|
||||||
|
accessCondition = AccessCondition.GenerateIfMatchCondition(blobRef.Properties.ETag);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
accessCondition = AccessCondition.GenerateIfNotExistsCondition();
|
||||||
|
blobRef.Properties.ContentType = "application/xml; charset=utf-8"; // set content type on first write
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Send the request up to the server.
|
||||||
|
|
||||||
|
var serializedDocAsByteArray = serializedDoc.ToArray();
|
||||||
|
|
||||||
|
await blobRef.UploadFromByteArrayAsync(
|
||||||
|
buffer: serializedDocAsByteArray,
|
||||||
|
index: 0,
|
||||||
|
count: serializedDocAsByteArray.Length,
|
||||||
|
accessCondition: accessCondition,
|
||||||
|
options: null,
|
||||||
|
operationContext: null);
|
||||||
|
|
||||||
|
// If we got this far, success!
|
||||||
|
// We can update the cached view of the remote contents.
|
||||||
|
|
||||||
|
Volatile.Write(ref _cachedBlobData, new BlobData()
|
||||||
|
{
|
||||||
|
BlobContents = serializedDocAsByteArray,
|
||||||
|
ETag = blobRef.Properties.ETag // was updated by Upload routine
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (StorageException ex)
|
||||||
|
when (ex.RequestInformation.HttpStatusCode == 409 || ex.RequestInformation.HttpStatusCode == 412)
|
||||||
|
{
|
||||||
|
// 409 Conflict
|
||||||
|
// This error is rare but can be thrown in very special circumstances,
|
||||||
|
// such as if the blob in the process of being created. We treat it
|
||||||
|
// as equivalent to 412 for the purposes of retry logic.
|
||||||
|
|
||||||
|
// 412 Precondition Failed
|
||||||
|
// We'll get this error if another writer updated the repository and we
|
||||||
|
// have an outdated view of its contents. If this occurs, we'll just
|
||||||
|
// refresh our view of the remote contents and try again up to the max
|
||||||
|
// retry limit.
|
||||||
|
|
||||||
|
lastError = ExceptionDispatchInfo.Capture(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we got this far, something went awry
|
||||||
|
lastError.Throw();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class BlobData
|
||||||
|
{
|
||||||
|
internal byte[] BlobContents;
|
||||||
|
internal string ETag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.DataProtection.Repositories;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.WindowsAzure.Storage;
|
||||||
|
using Microsoft.WindowsAzure.Storage.Auth;
|
||||||
|
using Microsoft.WindowsAzure.Storage.Blob;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.DataProtection.Azure.Blob
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains Azure-specific extension methods for modifying a
|
||||||
|
/// <see cref="IDataProtectionBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static class AzureDataProtectionBuilderExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the data protection system to persist keys to the specified path
|
||||||
|
/// in Azure Blob Storage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The builder instance to modify.</param>
|
||||||
|
/// <param name="storageAccount">The <see cref="CloudStorageAccount"/> which
|
||||||
|
/// should be utilized.</param>
|
||||||
|
/// <param name="relativePath">A relative path where the key file should be
|
||||||
|
/// stored, generally specified as "/containerName/[subDir/]keys.xml".</param>
|
||||||
|
/// <returns>The value <paramref name="builder"/>.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// The container referenced by <paramref name="relativePath"/> must already exist.
|
||||||
|
/// </remarks>
|
||||||
|
public static IDataProtectionBuilder PersistKeysToAzureBlobStorage(this IDataProtectionBuilder builder, CloudStorageAccount storageAccount, string relativePath)
|
||||||
|
{
|
||||||
|
if (builder == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(builder));
|
||||||
|
}
|
||||||
|
if (storageAccount == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(storageAccount));
|
||||||
|
}
|
||||||
|
if (relativePath == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(relativePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simply concatenate the root storage endpoint with the relative path,
|
||||||
|
// which includes the container name and blob name.
|
||||||
|
|
||||||
|
var uriBuilder = new UriBuilder(storageAccount.BlobEndpoint);
|
||||||
|
uriBuilder.Path = uriBuilder.Path.TrimEnd('/') + "/" + relativePath.TrimStart('/');
|
||||||
|
|
||||||
|
// We can create a CloudBlockBlob from the storage URI and the creds.
|
||||||
|
|
||||||
|
var blobAbsoluteUri = uriBuilder.Uri;
|
||||||
|
var credentials = storageAccount.Credentials;
|
||||||
|
|
||||||
|
return PersistKeystoAzureBlobStorageInternal(builder, () => new CloudBlockBlob(blobAbsoluteUri, credentials));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the data protection system to persist keys to the specified path
|
||||||
|
/// in Azure Blob Storage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The builder instance to modify.</param>
|
||||||
|
/// <param name="blobUri">The full URI where the key file should be stored.
|
||||||
|
/// The URI must contain the SAS token as a query string parameter.</param>
|
||||||
|
/// <returns>The value <paramref name="builder"/>.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// The container referenced by <paramref name="blobUri"/> must already exist.
|
||||||
|
/// </remarks>
|
||||||
|
public static IDataProtectionBuilder PersistKeysToAzureBlobStorage(this IDataProtectionBuilder builder, Uri blobUri)
|
||||||
|
{
|
||||||
|
if (builder == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(builder));
|
||||||
|
}
|
||||||
|
if (blobUri == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(blobUri));
|
||||||
|
}
|
||||||
|
|
||||||
|
var uriBuilder = new UriBuilder(blobUri);
|
||||||
|
|
||||||
|
// The SAS token is present in the query string.
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(uriBuilder.Query))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
message: "URI does not have a SAS token in the query string.",
|
||||||
|
paramName: nameof(blobUri));
|
||||||
|
}
|
||||||
|
|
||||||
|
var credentials = new StorageCredentials(uriBuilder.Query);
|
||||||
|
uriBuilder.Query = null; // no longer needed
|
||||||
|
var blobAbsoluteUri = uriBuilder.Uri;
|
||||||
|
|
||||||
|
return PersistKeystoAzureBlobStorageInternal(builder, () => new CloudBlockBlob(blobAbsoluteUri, credentials));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the data protection system to persist keys to the specified path
|
||||||
|
/// in Azure Blob Storage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The builder instance to modify.</param>
|
||||||
|
/// <param name="blobReference">The <see cref="CloudBlockBlob"/> where the
|
||||||
|
/// key file should be stored.</param>
|
||||||
|
/// <returns>The value <paramref name="builder"/>.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// The container referenced by <paramref name="blobReference"/> must already exist.
|
||||||
|
/// </remarks>
|
||||||
|
public static IDataProtectionBuilder PersistKeysToAzureBlobStorage(this IDataProtectionBuilder builder, CloudBlockBlob blobReference)
|
||||||
|
{
|
||||||
|
if (builder == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(builder));
|
||||||
|
}
|
||||||
|
if (blobReference == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(blobReference));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're basically just going to make a copy of this blob.
|
||||||
|
// Use (container, blobName) instead of (storageuri, creds) since the container
|
||||||
|
// is tied to an existing service client, which contains user-settable defaults
|
||||||
|
// like retry policy and secondary connection URIs.
|
||||||
|
|
||||||
|
var container = blobReference.Container;
|
||||||
|
var blobName = blobReference.Name;
|
||||||
|
|
||||||
|
return PersistKeystoAzureBlobStorageInternal(builder, () => container.GetBlockBlobReference(blobName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the data protection system to persist keys to the specified path
|
||||||
|
/// in Azure Blob Storage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The builder instance to modify.</param>
|
||||||
|
/// <param name="container">The <see cref="CloudBlobContainer"/> in which the
|
||||||
|
/// key file should be stored.</param>
|
||||||
|
/// <param name="blobName">The name of the key file, generally specified
|
||||||
|
/// as "[subdir/]keys.xml"</param>
|
||||||
|
/// <returns>The value <paramref name="builder"/>.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// The container referenced by <paramref name="container"/> must already exist.
|
||||||
|
/// </remarks>
|
||||||
|
public static IDataProtectionBuilder PersistKeysToAzureBlobStorage(this IDataProtectionBuilder builder, CloudBlobContainer container, string blobName)
|
||||||
|
{
|
||||||
|
if (builder == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(builder));
|
||||||
|
}
|
||||||
|
if (container == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(container));
|
||||||
|
}
|
||||||
|
if (blobName == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(blobName));
|
||||||
|
}
|
||||||
|
return PersistKeystoAzureBlobStorageInternal(builder, () => container.GetBlockBlobReference(blobName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// important: the Func passed into this method must return a new instance with each call
|
||||||
|
private static IDataProtectionBuilder PersistKeystoAzureBlobStorageInternal(IDataProtectionBuilder config, Func<CloudBlockBlob> blobRefFactory)
|
||||||
|
{
|
||||||
|
config.Services.AddSingleton<IXmlRepository>(services => new AzureBlobXmlRepository(blobRefFactory));
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="14.0.25420" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.25420</VisualStudioVersion>
|
||||||
|
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||||
|
<PropertyGroup Label="Globals">
|
||||||
|
<ProjectGuid>cc799b57-81e2-4f45-8a32-0d5f49753c3f</ProjectGuid>
|
||||||
|
<RootNamespace>Microsoft.AspNetCore.DataProtection.Azure</RootNamespace>
|
||||||
|
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||||
|
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Resources;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: AssemblyMetadata("Serviceable", "True")]
|
||||||
|
[assembly: NeutralResourcesLanguage("en-US")]
|
||||||
|
[assembly: AssemblyCompany("Microsoft Corporation.")]
|
||||||
|
[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")]
|
||||||
|
[assembly: AssemblyProduct("Microsoft ASP.NET Core")]
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"version": "0.1.0-*",
|
||||||
|
"description": "Microsoft Azure Blob storrage support as key store.",
|
||||||
|
"packOptions": {
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://github.com/aspnet/dataprotection"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"aspnetcore",
|
||||||
|
"dataprotection",
|
||||||
|
"azure",
|
||||||
|
"blob"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.AspNetCore.DataProtection": "1.1.0-*",
|
||||||
|
"WindowsAzure.Storage": "7.0.2-preview"
|
||||||
|
},
|
||||||
|
"frameworks": {
|
||||||
|
"net451": {},
|
||||||
|
"netstandard1.5": {
|
||||||
|
"imports": "portable-net45+win8+wp8+wpa81"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buildOptions": {
|
||||||
|
"allowUnsafe": true,
|
||||||
|
"warningsAsErrors": true,
|
||||||
|
"keyFile": "../../tools/Key.snk",
|
||||||
|
"nowarn": [
|
||||||
|
"CS1591"
|
||||||
|
],
|
||||||
|
"xmlDoc": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using Microsoft.AspNetCore.DataProtection.Azure.Blob;
|
||||||
|
using Microsoft.WindowsAzure.Storage;
|
||||||
|
using Microsoft.WindowsAzure.Storage.Blob;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.DataProtection.Azure.Test
|
||||||
|
{
|
||||||
|
public class AzureBlobXmlRepositoryTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void StoreCreatesBlobWhenNotExist()
|
||||||
|
{
|
||||||
|
AccessCondition downloadCondition = null;
|
||||||
|
AccessCondition uploadCondition = null;
|
||||||
|
byte[] bytes = null;
|
||||||
|
BlobProperties properties = new BlobProperties();
|
||||||
|
|
||||||
|
var mock = new Mock<ICloudBlob>();
|
||||||
|
mock.SetupGet(c => c.Properties).Returns(properties);
|
||||||
|
mock.Setup(c => c.UploadFromByteArrayAsync(
|
||||||
|
It.IsAny<byte[]>(),
|
||||||
|
It.IsAny<int>(),
|
||||||
|
It.IsAny<int>(),
|
||||||
|
It.IsAny<AccessCondition>(),
|
||||||
|
It.IsAny<BlobRequestOptions>(),
|
||||||
|
It.IsAny<OperationContext>()))
|
||||||
|
.Returns(async (byte[] buffer, int index, int count, AccessCondition accessCondition, BlobRequestOptions options, OperationContext operationContext) =>
|
||||||
|
{
|
||||||
|
bytes = buffer.Skip(index).Take(count).ToArray();
|
||||||
|
uploadCondition = accessCondition;
|
||||||
|
await Task.Yield();
|
||||||
|
});
|
||||||
|
|
||||||
|
var repository = new AzureBlobXmlRepository(() => mock.Object);
|
||||||
|
repository.StoreElement(new XElement("Element"), null);
|
||||||
|
|
||||||
|
Assert.Null(downloadCondition);
|
||||||
|
Assert.Equal("*", uploadCondition.IfNoneMatchETag);
|
||||||
|
Assert.Equal("application/xml; charset=utf-8", properties.ContentType);
|
||||||
|
var element = "<Element />";
|
||||||
|
|
||||||
|
Assert.Equal(bytes, GetEnvelopedContent(element));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StoreUpdatesWhenExistsAndNewerExists()
|
||||||
|
{
|
||||||
|
AccessCondition downloadCondition = null;
|
||||||
|
byte[] bytes = null;
|
||||||
|
BlobProperties properties = new BlobProperties();
|
||||||
|
|
||||||
|
var mock = new Mock<ICloudBlob>();
|
||||||
|
mock.SetupGet(c => c.Properties).Returns(properties);
|
||||||
|
mock.Setup(c => c.DownloadToStreamAsync(
|
||||||
|
It.IsAny<Stream>(),
|
||||||
|
It.IsAny<AccessCondition>(),
|
||||||
|
null,
|
||||||
|
null))
|
||||||
|
.Returns(async (Stream target, AccessCondition condition, BlobRequestOptions options, OperationContext context) =>
|
||||||
|
{
|
||||||
|
var data = GetEnvelopedContent("<Element1 />");
|
||||||
|
await target.WriteAsync(data, 0, data.Length);
|
||||||
|
})
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
mock.Setup(c => c.UploadFromByteArrayAsync(
|
||||||
|
It.IsAny<byte[]>(),
|
||||||
|
It.IsAny<int>(),
|
||||||
|
It.IsAny<int>(),
|
||||||
|
It.Is((AccessCondition cond) => cond.IfNoneMatchETag == "*"),
|
||||||
|
It.IsAny<BlobRequestOptions>(),
|
||||||
|
It.IsAny<OperationContext>()))
|
||||||
|
.Throws(new StorageException(new RequestResult { HttpStatusCode = 412 }, null, null))
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
mock.Setup(c => c.UploadFromByteArrayAsync(
|
||||||
|
It.IsAny<byte[]>(),
|
||||||
|
It.IsAny<int>(),
|
||||||
|
It.IsAny<int>(),
|
||||||
|
It.Is((AccessCondition cond) => cond.IfNoneMatchETag != "*"),
|
||||||
|
It.IsAny<BlobRequestOptions>(),
|
||||||
|
It.IsAny<OperationContext>()))
|
||||||
|
.Returns(async (byte[] buffer, int index, int count, AccessCondition accessCondition, BlobRequestOptions options, OperationContext operationContext) =>
|
||||||
|
{
|
||||||
|
bytes = buffer.Skip(index).Take(count).ToArray();
|
||||||
|
await Task.Yield();
|
||||||
|
})
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var repository = new AzureBlobXmlRepository(() => mock.Object);
|
||||||
|
repository.StoreElement(new XElement("Element2"), null);
|
||||||
|
|
||||||
|
mock.Verify();
|
||||||
|
Assert.Null(downloadCondition);
|
||||||
|
Assert.Equal(bytes, GetEnvelopedContent("<Element1 /><Element2 />"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] GetEnvelopedContent(string element)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes($"<?xml version=\"1.0\" encoding=\"utf-8\"?><repository>{element}</repository>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="14.0.25420" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.25420</VisualStudioVersion>
|
||||||
|
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||||
|
<PropertyGroup Label="Globals">
|
||||||
|
<ProjectGuid>8c41240e-48f8-402f-9388-74cfe27f4d76</ProjectGuid>
|
||||||
|
<RootNamespace>Microsoft.AspNetCore.DataProtection.Azure.Test</RootNamespace>
|
||||||
|
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||||
|
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"dotnet-test-xunit": "2.2.0-*",
|
||||||
|
"Microsoft.AspNetCore.DataProtection": "1.1.0-*",
|
||||||
|
"Microsoft.AspNetCore.DataProtection.Azure.Blob": "1.1.0-*",
|
||||||
|
"Microsoft.AspNetCore.Testing": "1.1.0-*",
|
||||||
|
"Microsoft.Extensions.DependencyInjection": "1.1.0-*",
|
||||||
|
"xunit": "2.2.0-*",
|
||||||
|
"Moq": "4.6.36-*"
|
||||||
|
},
|
||||||
|
"frameworks": {
|
||||||
|
"netcoreapp1.0": {
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.NETCore.App": {
|
||||||
|
"version": "1.0.0-*",
|
||||||
|
"type": "platform"
|
||||||
|
},
|
||||||
|
"System.Diagnostics.Process": "4.1.0-*",
|
||||||
|
"System.Diagnostics.TraceSource": "4.0.0-*"
|
||||||
|
},
|
||||||
|
"imports": [
|
||||||
|
"dnxcore50",
|
||||||
|
"portable-net451+win8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"net451": {
|
||||||
|
"frameworkAssemblies": {
|
||||||
|
"System.Threading.Tasks": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"testRunner": "xunit",
|
||||||
|
"buildOptions": {
|
||||||
|
"allowUnsafe": true,
|
||||||
|
"warningsAsErrors": true,
|
||||||
|
"keyFile": "../../tools/Key.snk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,9 +11,11 @@
|
||||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<SchemaVersion>2.0</SchemaVersion>
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||||
</Project>
|
</Project>
|
||||||
Loading…
Reference in New Issue