Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -68,26 +68,17 @@ public void EnsureDefaults(IConfigurationBuilder builder)
}

/// <summary>
/// Creates a physical file provider for the nearest existing directory if no file provider has been set, for absolute Path.
/// Creates a physical file provider for the file's directory if no file provider has been set, for absolute Path.
/// </summary>
public void ResolveFileProvider()
{
if (FileProvider == null &&
!string.IsNullOrEmpty(Path) &&
System.IO.Path.IsPathRooted(Path))
System.IO.Path.IsPathRooted(Path) &&
System.IO.Path.GetDirectoryName(Path) is string directory)
{
string? directory = System.IO.Path.GetDirectoryName(Path);
string? pathToFile = System.IO.Path.GetFileName(Path);
while (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
pathToFile = System.IO.Path.Combine(System.IO.Path.GetFileName(directory), pathToFile);
directory = System.IO.Path.GetDirectoryName(directory);
}
if (Directory.Exists(directory))
{
FileProvider = new PhysicalFileProvider(directory);
Path = pathToFile;
}
FileProvider = new PhysicalFileProvider(directory);
Path = System.IO.Path.GetFileName(Path);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration.Test;
using Microsoft.Extensions.FileProviders;
using Moq;
Expand Down Expand Up @@ -112,6 +114,56 @@ public void ProviderThrowsDirectoryNotFoundExceptionWhenNotFound(string physical
Assert.Contains(physicalPath, exception.Message);
}

[Fact]
public async Task ResolveFileProvider_WithMissingParentDirectory_WatchTokenFiresWhenFileCreated()
{
// Verify the fix for https://github.com/dotnet/runtime/issues/116713:
// When the parent of the config file does not yet exist, Watch() should return a change token
// that fires when the target file is created (via a non-recursive pending watcher),
// rather than adding recursive watches on the entire ancestor directory tree.
using var rootDir = new TempDirectory(Path.Combine(Path.GetTempPath(), $"pfp_cfg_test_{Guid.NewGuid():N}"));
string missingSubDir = Path.Combine(rootDir.Path, "subdir");
string configFilePath = Path.Combine(missingSubDir, "appsettings.json");

var source = new FileConfigurationSourceImpl
{
Path = configFilePath,
Optional = true,
ReloadOnChange = true,
ReloadDelay = 0,
};

// ResolveFileProvider sets FileProvider to the directory containing the file path,
// even if that directory does not yet exist on disk.
source.ResolveFileProvider();

Assert.NotNull(source.FileProvider);
using var physicalProvider = Assert.IsType<PhysicalFileProvider>(source.FileProvider);
Assert.Equal(missingSubDir + Path.DirectorySeparatorChar, physicalProvider.Root);

// The configuration Path is reduced to the file name relative to the provider root.
// Verify that the intermediate directory name is not part of Path.
Assert.DoesNotContain("subdir", source.Path, StringComparison.OrdinalIgnoreCase);

// Watch() must return a valid (non-null) change token even though the directory is missing.
var token = source.FileProvider.Watch(source.Path);
Assert.NotNull(token);

// The token should fire only when the target file is created, not when just the directory appears.
var tcs = new TaskCompletionSource<bool>();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
cts.Token.Register(() => tcs.TrySetCanceled());
token.RegisterChangeCallback(_ => tcs.TrySetResult(true), null);

Directory.CreateDirectory(missingSubDir);
await Task.Delay(500);
Assert.False(tcs.Task.IsCompleted, "Token must not fire when only the directory is created.");

File.WriteAllText(configFilePath, "{}");

Assert.True(await tcs.Task, "Change token did not fire after the target file was created.");
}

public class FileInfoImpl : IFileInfo
{
public FileInfoImpl(string physicalPath, bool exists = true) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

<ItemGroup>
<Compile Include="$(CommonTestPath)Extensions\ConfigurationRootTest.cs" Link="Common\Extensions\ConfigurationRootTest.cs" />
<Compile Include="$(CommonTestPath)System\IO\TempDirectory.cs" Link="Common\System\IO\TempDirectory.cs" />

<TrimmerRootDescriptor Include="$(ILLinkDescriptorsPath)ILLink.Descriptors.Castle.xml" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ internal static bool HasInvalidPathChars(string path) =>
internal static bool HasInvalidFilterChars(string path) =>
path.AsSpan().ContainsAny(_invalidFilterChars);

private static readonly char[] _pathSeparators = new[]
{Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar};
internal static readonly char[] PathSeparators =
[Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar];

internal static string EnsureTrailingSlash(string path)
{
if (!string.IsNullOrEmpty(path) &&
path[path.Length - 1] != Path.DirectorySeparatorChar)
path[path.Length - 1] != Path.DirectorySeparatorChar &&
path[path.Length - 1] != Path.AltDirectorySeparatorChar)
{
return path + Path.DirectorySeparatorChar;
}
Expand All @@ -42,7 +43,7 @@ internal static string EnsureTrailingSlash(string path)

internal static bool PathNavigatesAboveRoot(string path)
{
var tokenizer = new StringTokenizer(path, _pathSeparators);
var tokenizer = new StringTokenizer(path, PathSeparators);
int depth = 0;

foreach (StringSegment segment in tokenizer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ namespace Microsoft.Extensions.FileProviders
public class PhysicalFileProvider : IFileProvider, IDisposable
{
private const string PollingEnvironmentKey = "DOTNET_USE_POLLING_FILE_WATCHER";
private static readonly char[] _pathSeparators = new[]
{Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar};

private readonly ExclusionFilters _filters;

Expand Down Expand Up @@ -61,14 +59,6 @@ public PhysicalFileProvider(string root, ExclusionFilters filters)
string fullRoot = Path.GetFullPath(root);
// When we do matches in GetFullPath, we want to only match full directory names.
Root = PathUtils.EnsureTrailingSlash(fullRoot);
if (!Directory.Exists(Root))
{
#if NET11_0_OR_GREATER
throw new DirectoryNotFoundException(null, Root);
#else
throw new DirectoryNotFoundException(Root);
#endif
}

_filters = filters;
_fileWatcherFactory = CreateFileWatcher;
Expand Down Expand Up @@ -178,7 +168,7 @@ internal PhysicalFilesWatcher CreateFileWatcher()
#endif
{
// When UsePollingFileWatcher & UseActivePolling are set, we won't use a FileSystemWatcher.
watcher = UsePollingFileWatcher && UseActivePolling ? null : new FileSystemWatcher(root);
watcher = UsePollingFileWatcher && UseActivePolling ? null : new FileSystemWatcher();
}

return new PhysicalFilesWatcher(root, watcher, UsePollingFileWatcher, _filters)
Expand Down Expand Up @@ -272,7 +262,7 @@ public IFileInfo GetFileInfo(string subpath)
}

// Relative paths starting with leading slashes are okay
subpath = subpath.TrimStart(_pathSeparators);
subpath = subpath.TrimStart(PathUtils.PathSeparators);

// Absolute paths not permitted.
if (Path.IsPathRooted(subpath))
Expand Down Expand Up @@ -317,7 +307,7 @@ public IDirectoryContents GetDirectoryContents(string subpath)
}

// Relative paths starting with leading slashes are okay
subpath = subpath.TrimStart(_pathSeparators);
subpath = subpath.TrimStart(PathUtils.PathSeparators);

// Absolute paths not permitted.
if (Path.IsPathRooted(subpath))
Expand Down Expand Up @@ -347,11 +337,11 @@ public IDirectoryContents GetDirectoryContents(string subpath)
/// <para>Globbing patterns are interpreted by <see cref="Microsoft.Extensions.FileSystemGlobbing.Matcher" />.</para>
/// </summary>
/// <param name="filter">
/// Filter string used to determine what files or folders to monitor. Example: **/*.cs, *.*,
/// subFolder/**/*.cshtml.
/// Filter string used to determine what files or directories to monitor. Example: **/*.cs, *.*,
/// subDirectory/**/*.cshtml.
/// </param>
/// <returns>
/// An <see cref="IChangeToken" /> that is notified when a file matching <paramref name="filter" /> is added,
/// An <see cref="IChangeToken" /> that is notified when a file or directory matching <paramref name="filter" /> is added,
/// modified, or deleted. Returns a <see cref="NullChangeToken" /> if <paramref name="filter" /> has invalid filter
/// characters or if <paramref name="filter" /> is an absolute path or outside the root directory specified in the
/// constructor <see cref="PhysicalFileProvider(string)" />.
Expand All @@ -364,7 +354,7 @@ public IChangeToken Watch(string filter)
}

// Relative paths starting with leading slashes are okay
filter = filter.TrimStart(_pathSeparators);
filter = filter.TrimStart(PathUtils.PathSeparators);

return FileWatcher.CreateFileChangeToken(filter);
}
Expand Down
Loading
Loading