Merge source code from aspnet/Configuration into this repo
\n\nCommit migrated from 8a9b64a3e5
This commit is contained in:
commit
8d17a5b9f3
|
|
@ -0,0 +1,8 @@
|
|||
<Project>
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile Condition=" '$(IsTestProject)' != 'true' AND '$(IsSampleProject)' != 'true' ">true</GenerateDocumentationFile>
|
||||
<PackageTags>configuration</PackageTags>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Configuration.KeyPerFile;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
namespace Microsoft.Extensions.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for registering <see cref="KeyPerFileConfigurationProvider"/> with <see cref="IConfigurationBuilder"/>.
|
||||
/// </summary>
|
||||
public static class KeyPerFileConfigurationBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds configuration using files from a directory. File names are used as the key,
|
||||
/// file contents are used as the value.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
|
||||
/// <param name="directoryPath">The path to the directory.</param>
|
||||
/// <param name="optional">Whether the directory is optional.</param>
|
||||
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
|
||||
public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath, bool optional)
|
||||
=> builder.AddKeyPerFile(source =>
|
||||
{
|
||||
// Only try to set the file provider if its not optional or the directory exists
|
||||
if (!optional || Directory.Exists(directoryPath))
|
||||
{
|
||||
source.FileProvider = new PhysicalFileProvider(directoryPath);
|
||||
}
|
||||
source.Optional = optional;
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Adds configuration using files from a directory. File names are used as the key,
|
||||
/// file contents are used as the value.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
|
||||
/// <param name="configureSource">Configures the source.</param>
|
||||
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
|
||||
public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, Action<KeyPerFileConfigurationSource> configureSource)
|
||||
=> builder.Add(configureSource);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.Extensions.Configuration.KeyPerFile
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="ConfigurationProvider"/> that uses a directory's files as configuration key/values.
|
||||
/// </summary>
|
||||
public class KeyPerFileConfigurationProvider : ConfigurationProvider
|
||||
{
|
||||
KeyPerFileConfigurationSource Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance.
|
||||
/// </summary>
|
||||
/// <param name="source">The settings.</param>
|
||||
public KeyPerFileConfigurationProvider(KeyPerFileConfigurationSource source)
|
||||
=> Source = source ?? throw new ArgumentNullException(nameof(source));
|
||||
|
||||
private static string NormalizeKey(string key)
|
||||
=> key.Replace("__", ConfigurationPath.KeyDelimiter);
|
||||
|
||||
private static string TrimNewLine(string value)
|
||||
=> value.EndsWith(Environment.NewLine)
|
||||
? value.Substring(0, value.Length - Environment.NewLine.Length)
|
||||
: value;
|
||||
|
||||
/// <summary>
|
||||
/// Loads the docker secrets.
|
||||
/// </summary>
|
||||
public override void Load()
|
||||
{
|
||||
Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (Source.FileProvider == null)
|
||||
{
|
||||
if (Source.Optional)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new DirectoryNotFoundException("A non-null file provider for the directory is required when this source is not optional.");
|
||||
}
|
||||
}
|
||||
|
||||
var directory = Source.FileProvider.GetDirectoryContents("/");
|
||||
if (!directory.Exists && !Source.Optional)
|
||||
{
|
||||
throw new DirectoryNotFoundException("The root directory for the FileProvider doesn't exist and is not optional.");
|
||||
}
|
||||
|
||||
foreach (var file in directory)
|
||||
{
|
||||
if (file.IsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using (var stream = file.CreateReadStream())
|
||||
using (var streamReader = new StreamReader(stream))
|
||||
{
|
||||
if (Source.IgnoreCondition == null || !Source.IgnoreCondition(file.Name))
|
||||
{
|
||||
Data.Add(NormalizeKey(file.Name), TrimNewLine(streamReader.ReadToEnd()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
namespace Microsoft.Extensions.Configuration.KeyPerFile
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IConfigurationSource"/> used to configure <see cref="KeyPerFileConfigurationProvider"/>.
|
||||
/// </summary>
|
||||
public class KeyPerFileConfigurationSource : IConfigurationSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor;
|
||||
/// </summary>
|
||||
public KeyPerFileConfigurationSource()
|
||||
=> IgnoreCondition = s => IgnorePrefix != null && s.StartsWith(IgnorePrefix);
|
||||
|
||||
/// <summary>
|
||||
/// The FileProvider whos root "/" directory files will be used as configuration data.
|
||||
/// </summary>
|
||||
public IFileProvider FileProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Files that start with this prefix will be excluded.
|
||||
/// Defaults to "ignore.".
|
||||
/// </summary>
|
||||
public string IgnorePrefix { get; set; } = "ignore.";
|
||||
|
||||
/// <summary>
|
||||
/// Used to determine if a file should be ignored using its name.
|
||||
/// Defaults to using the IgnorePrefix.
|
||||
/// </summary>
|
||||
public Func<string, bool> IgnoreCondition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If false, will throw if the directory doesn't exist.
|
||||
/// </summary>
|
||||
public bool Optional { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Builds the <see cref="KeyPerFileConfigurationProvider"/> for this source.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param>
|
||||
/// <returns>A <see cref="KeyPerFileConfigurationProvider"/></returns>
|
||||
public IConfigurationProvider Build(IConfigurationBuilder builder)
|
||||
=> new KeyPerFileConfigurationProvider(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Configuration provider that uses files in a directory for Microsoft.Extensions.Configuration.</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<EnableApiCheck>false</EnableApiCheck>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.Configuration" />
|
||||
<Reference Include="Microsoft.Extensions.FileProviders.Physical" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
This is a configuration provider that uses a directory's files as data. A file's name is the key and the contents are the value.
|
||||
|
|
@ -0,0 +1,308 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
|
||||
{
|
||||
public class KeyPerFileTests
|
||||
{
|
||||
[Fact]
|
||||
public void DoesNotThrowWhenOptionalAndNoSecrets()
|
||||
{
|
||||
new ConfigurationBuilder().AddKeyPerFile(o => o.Optional = true).Build();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotThrowWhenOptionalAndDirectoryDoesntExist()
|
||||
{
|
||||
new ConfigurationBuilder().AddKeyPerFile("nonexistent", true).Build();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowsWhenNotOptionalAndDirectoryDoesntExist()
|
||||
{
|
||||
var e = Assert.Throws<ArgumentException>(() => new ConfigurationBuilder().AddKeyPerFile("nonexistent", false).Build());
|
||||
Assert.Contains("The directory name", e.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanLoadMultipleSecrets()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("Secret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o => o.FileProvider = testFileProvider)
|
||||
.Build();
|
||||
|
||||
Assert.Equal("SecretValue1", config["Secret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanLoadMultipleSecretsWithDirectory()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("Secret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"),
|
||||
new TestFile("directory"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o => o.FileProvider = testFileProvider)
|
||||
.Build();
|
||||
|
||||
Assert.Equal("SecretValue1", config["Secret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanLoadNestedKeys()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("Secret0__Secret1__Secret2__Key", "SecretValue2"),
|
||||
new TestFile("Secret0__Secret1__Key", "SecretValue1"),
|
||||
new TestFile("Secret0__Key", "SecretValue0"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o => o.FileProvider = testFileProvider)
|
||||
.Build();
|
||||
|
||||
Assert.Equal("SecretValue0", config["Secret0:Key"]);
|
||||
Assert.Equal("SecretValue1", config["Secret0:Secret1:Key"]);
|
||||
Assert.Equal("SecretValue2", config["Secret0:Secret1:Secret2:Key"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanIgnoreFilesWithDefault()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("ignore.Secret0", "SecretValue0"),
|
||||
new TestFile("ignore.Secret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o => o.FileProvider = testFileProvider)
|
||||
.Build();
|
||||
|
||||
Assert.Null(config["ignore.Secret0"]);
|
||||
Assert.Null(config["ignore.Secret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanTurnOffDefaultIgnorePrefixWithCondition()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("ignore.Secret0", "SecretValue0"),
|
||||
new TestFile("ignore.Secret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o =>
|
||||
{
|
||||
o.FileProvider = testFileProvider;
|
||||
o.IgnoreCondition = null;
|
||||
})
|
||||
.Build();
|
||||
|
||||
Assert.Equal("SecretValue0", config["ignore.Secret0"]);
|
||||
Assert.Equal("SecretValue1", config["ignore.Secret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanIgnoreAllWithCondition()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("Secret0", "SecretValue0"),
|
||||
new TestFile("Secret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o =>
|
||||
{
|
||||
o.FileProvider = testFileProvider;
|
||||
o.IgnoreCondition = s => true;
|
||||
})
|
||||
.Build();
|
||||
|
||||
Assert.Empty(config.AsEnumerable());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanIgnoreFilesWithCustomIgnore()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("meSecret0", "SecretValue0"),
|
||||
new TestFile("meSecret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o =>
|
||||
{
|
||||
o.FileProvider = testFileProvider;
|
||||
o.IgnorePrefix = "me";
|
||||
})
|
||||
.Build();
|
||||
|
||||
Assert.Null(config["meSecret0"]);
|
||||
Assert.Null(config["meSecret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUnIgnoreDefaultFiles()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("ignore.Secret0", "SecretValue0"),
|
||||
new TestFile("ignore.Secret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o =>
|
||||
{
|
||||
o.FileProvider = testFileProvider;
|
||||
o.IgnorePrefix = null;
|
||||
})
|
||||
.Build();
|
||||
|
||||
Assert.Equal("SecretValue0", config["ignore.Secret0"]);
|
||||
Assert.Equal("SecretValue1", config["ignore.Secret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
}
|
||||
}
|
||||
|
||||
class TestFileProvider : IFileProvider
|
||||
{
|
||||
IDirectoryContents _contents;
|
||||
|
||||
public TestFileProvider(params IFileInfo[] files)
|
||||
{
|
||||
_contents = new TestDirectoryContents(files);
|
||||
}
|
||||
|
||||
public IDirectoryContents GetDirectoryContents(string subpath)
|
||||
{
|
||||
return _contents;
|
||||
}
|
||||
|
||||
public IFileInfo GetFileInfo(string subpath)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IChangeToken Watch(string filter)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
class TestDirectoryContents : IDirectoryContents
|
||||
{
|
||||
List<IFileInfo> _list;
|
||||
|
||||
public TestDirectoryContents(params IFileInfo[] files)
|
||||
{
|
||||
_list = new List<IFileInfo>(files);
|
||||
}
|
||||
|
||||
public bool Exists
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<IFileInfo> GetEnumerator()
|
||||
{
|
||||
return _list.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Probably need a directory and file type.
|
||||
class TestFile : IFileInfo
|
||||
{
|
||||
private string _name;
|
||||
private string _contents;
|
||||
|
||||
public bool Exists
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDirectory
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public DateTimeOffset LastModified
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return _name;
|
||||
}
|
||||
}
|
||||
|
||||
public string PhysicalPath
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public TestFile(string name)
|
||||
{
|
||||
_name = name;
|
||||
IsDirectory = true;
|
||||
}
|
||||
|
||||
public TestFile(string name, string contents)
|
||||
{
|
||||
_name = name;
|
||||
_contents = contents;
|
||||
}
|
||||
|
||||
public Stream CreateReadStream()
|
||||
{
|
||||
if(IsDirectory)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot create stream from directory");
|
||||
}
|
||||
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes(_contents));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.Configuration.KeyPerFile" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
Reference in New Issue