diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Abstractions/ref/Microsoft.Extensions.FileProviders.Abstractions.csproj b/src/libraries/Microsoft.Extensions.FileProviders.Abstractions/ref/Microsoft.Extensions.FileProviders.Abstractions.csproj index 32239798f1f1e2..bd02f83be9189b 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Abstractions/ref/Microsoft.Extensions.FileProviders.Abstractions.csproj +++ b/src/libraries/Microsoft.Extensions.FileProviders.Abstractions/ref/Microsoft.Extensions.FileProviders.Abstractions.csproj @@ -1,6 +1,6 @@ - netstandard2.0;net461 + $(NetCoreAppCurrent);netstandard2.0;net461 @@ -8,4 +8,7 @@ + + + diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Abstractions/src/Microsoft.Extensions.FileProviders.Abstractions.csproj b/src/libraries/Microsoft.Extensions.FileProviders.Abstractions/src/Microsoft.Extensions.FileProviders.Abstractions.csproj index 43164c743a994f..07289f58670e5b 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Abstractions/src/Microsoft.Extensions.FileProviders.Abstractions.csproj +++ b/src/libraries/Microsoft.Extensions.FileProviders.Abstractions/src/Microsoft.Extensions.FileProviders.Abstractions.csproj @@ -2,7 +2,7 @@ Microsoft.Extensions.FileProviders - netstandard2.0;net461 + $(NetCoreAppCurrent);netstandard2.0;net461 true Abstractions of files and directories. @@ -21,4 +21,9 @@ Microsoft.Extensions.FileProviders.IFileProvider + + + + + diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/Directory.Build.props b/src/libraries/Microsoft.Extensions.FileProviders.Physical/Directory.Build.props index 668e3954f0b426..8ec207de7d8b3f 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/Directory.Build.props +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/Directory.Build.props @@ -2,5 +2,6 @@ true + browser \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/ref/Microsoft.Extensions.FileProviders.Physical.csproj b/src/libraries/Microsoft.Extensions.FileProviders.Physical/ref/Microsoft.Extensions.FileProviders.Physical.csproj index cb4948ba130acb..0f7dc64cb84a29 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/ref/Microsoft.Extensions.FileProviders.Physical.csproj +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/ref/Microsoft.Extensions.FileProviders.Physical.csproj @@ -1,6 +1,6 @@ - netstandard2.0;net461 + $(NetCoreAppCurrent);netstandard2.0;net461 @@ -10,4 +10,7 @@ + + + diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Internal/FileSystemInfoHelper.cs b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Internal/FileSystemInfoHelper.cs index 067a1545860e2a..5ae854ae6efce8 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Internal/FileSystemInfoHelper.cs +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Internal/FileSystemInfoHelper.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics; using System.IO; namespace Microsoft.Extensions.FileProviders.Physical @@ -27,5 +28,40 @@ public static bool IsExcluded(FileSystemInfo fileSystemInfo, ExclusionFilters fi return false; } + + public static DateTime? GetFileLinkTargetLastWriteTimeUtc(string filePath) + { +#if NETCOREAPP + var fileInfo = new FileInfo(filePath); + if (fileInfo.Exists) + { + return GetFileLinkTargetLastWriteTimeUtc(fileInfo); + } +#endif + return null; + } + + // If file is a link and link target exists, return target's LastWriteTimeUtc. + // If file is a link, and link target does not exists, return DateTime.MinValue + // since the link's LastWriteTimeUtc doesn't convey anything for this scenario. + // If file is not a link, return null to inform the caller that file is not a link. + public static DateTime? GetFileLinkTargetLastWriteTimeUtc(FileInfo fileInfo) + { +#if NETCOREAPP + Debug.Assert(fileInfo.Exists); + if (fileInfo.LinkTarget != null) + { + FileSystemInfo targetInfo = fileInfo.ResolveLinkTarget(returnFinalTarget: true); + if (targetInfo.Exists) + { + return targetInfo.LastWriteTimeUtc; + } + + return DateTime.MinValue; + } +#endif + + return null; + } } } diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Microsoft.Extensions.FileProviders.Physical.csproj b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Microsoft.Extensions.FileProviders.Physical.csproj index 9801bac9fead45..534a08b11ff87a 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Microsoft.Extensions.FileProviders.Physical.csproj +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Microsoft.Extensions.FileProviders.Physical.csproj @@ -1,12 +1,17 @@ - + Microsoft.Extensions.FileProviders - netstandard2.0;net461 + $(NetCoreAppCurrent);$(NetCoreAppCurrent)-Browser;netstandard2.0;net461 true true File provider for physical files for Microsoft.Extensions.FileProviders. + + + false + SR.FileProvidersPhysical_PlatformNotSupported + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PollingFileChangeToken.cs b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PollingFileChangeToken.cs index 23d8c8d306c74c..ddf2ae07423f0a 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PollingFileChangeToken.cs +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PollingFileChangeToken.cs @@ -48,7 +48,13 @@ public PollingFileChangeToken(FileInfo fileInfo) private DateTime GetLastWriteTimeUtc() { _fileInfo.Refresh(); - return _fileInfo.Exists ? _fileInfo.LastWriteTimeUtc : DateTime.MinValue; + + if (!_fileInfo.Exists) + { + return DateTime.MinValue; + } + + return FileSystemInfoHelper.GetFileLinkTargetLastWriteTimeUtc(_fileInfo) ?? _fileInfo.LastWriteTimeUtc; } /// diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PollingWildCardChangeToken.cs b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PollingWildCardChangeToken.cs index 998aaa29772fef..28148d2315b966 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PollingWildCardChangeToken.cs +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PollingWildCardChangeToken.cs @@ -143,7 +143,8 @@ private bool CalculateChanges() /// The that the file was last modified. protected virtual DateTime GetLastWriteUtc(string path) { - return File.GetLastWriteTimeUtc(Path.Combine(_directoryInfo.FullName, path)); + string filePath = Path.Combine(_directoryInfo.FullName, path); + return FileSystemInfoHelper.GetFileLinkTargetLastWriteTimeUtc(filePath) ?? File.GetLastWriteTimeUtc(filePath); } private static bool ArrayEquals(byte[] previousHash, byte[] currentHash) diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Resources/Strings.resx index 4d234ccabe2024..2ff2106bca01fe 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Resources/Strings.resx @@ -129,4 +129,7 @@ Unexpected type of FileSystemInfo + + Microsoft.Extensions.FileProviders.Physical is not supported on this platform. + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/Microsoft.Extensions.FileProviders.Physical.Tests.csproj b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/Microsoft.Extensions.FileProviders.Physical.Tests.csproj index d57fed0ca59497..6b4ab37b34045f 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/Microsoft.Extensions.FileProviders.Physical.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/Microsoft.Extensions.FileProviders.Physical.Tests.csproj @@ -1,10 +1,11 @@ - + Microsoft.Extensions.FileProviders.Physical $(NetCoreAppCurrent);net461 true true + false @@ -12,6 +13,10 @@ Link="Common\System\Threading\Tasks\TaskTimeoutExtensions.cs" /> + + + + diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs index 5bb882e5cccd9c..b4e37df4f11636 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.FileProviders { - public class PhysicalFileProviderTests + public partial class PhysicalFileProviderTests { private const int WaitTimeForTokenToFire = 500; private const int WaitTimeForTokenCallback = 10000; @@ -1512,6 +1512,58 @@ public void UsePollingFileWatcher_FileWatcherNotNull_ReturnsFalse() } } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UsePollingFileWatcher_UseActivePolling_HasChanged(bool useWildcard) + { + // Arrange + using var root = new DisposableFileSystem(); + string fileName = Path.GetRandomFileName(); + string filePath = Path.Combine(root.RootPath, fileName); + File.WriteAllText(filePath, "v1.1"); + + using var provider = new PhysicalFileProvider(root.RootPath) { UsePollingFileWatcher = true, UseActivePolling = true }; + IChangeToken token = provider.Watch(useWildcard ? "*" : fileName); + + var tcs = new TaskCompletionSource(); + token.RegisterChangeCallback(_ => { tcs.TrySetResult(null); }, null); + + // Act + await Task.Delay(1000); // Wait a second before writing again, see https://github.com/dotnet/runtime/issues/55951. + File.WriteAllText(filePath, "v1.2"); + + // Assert + Assert.True(tcs.Task.Wait(TimeSpan.FromSeconds(30)), + $"Change event was not raised - current time: {DateTime.UtcNow:O}, file LastWriteTimeUtc: {File.GetLastWriteTimeUtc(filePath):O}"); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void UsePollingFileWatcher_UseActivePolling_HasChanged_FileDeleted(bool useWildcard) + { + // Arrange + using var root = new DisposableFileSystem(); + string fileName = Path.GetRandomFileName(); + string filePath = Path.Combine(root.RootPath, fileName); + File.WriteAllText(filePath, "v1.1"); + + string filter = useWildcard ? "*" : fileName; + using var provider = new PhysicalFileProvider(root.RootPath) { UsePollingFileWatcher = true, UseActivePolling = true }; + IChangeToken token = provider.Watch(filter); + + var tcs = new TaskCompletionSource(); + token.RegisterChangeCallback(_ => { tcs.TrySetResult(null); }, null); + + // Act + File.Delete(filePath); + + // Assert + Assert.True(tcs.Task.Wait(TimeSpan.FromSeconds(30)), + $"Change event was not raised - current time: {DateTime.UtcNow:O}, file Exists: {File.Exists(filePath)}."); + } + [Fact] public void CreateFileWatcher_CreatesWatcherWithPollingAndActiveFlags() { diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.netcoreapp.cs b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.netcoreapp.cs new file mode 100644 index 00000000000000..d8b332b6409f56 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.netcoreapp.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.Extensions.FileProviders +{ + public partial class PhysicalFileProviderTests + { + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UsePollingFileWatcher_UseActivePolling_HasChanged_SymbolicLink(bool useWildcard) + { + // Arrange + using var rootOfFile = new DisposableFileSystem(); + string filePath = Path.Combine(rootOfFile.RootPath, Path.GetRandomFileName()); + File.WriteAllText(filePath, "v1.1"); + + using var rootOfLink = new DisposableFileSystem(); + string linkName = Path.GetRandomFileName(); + string linkPath = Path.Combine(rootOfLink.RootPath, linkName); + File.CreateSymbolicLink(linkPath, filePath); + + using var provider = new PhysicalFileProvider(rootOfLink.RootPath) { UsePollingFileWatcher = true, UseActivePolling = true }; + IChangeToken token = provider.Watch(useWildcard ? "*" : linkName); + + var tcs = new TaskCompletionSource(); + token.RegisterChangeCallback(_ => { tcs.TrySetResult(); }, null); + + // Act + await Task.Delay(1000); // Wait a second before writing again, see https://github.com/dotnet/runtime/issues/55951. + File.WriteAllText(filePath, "v1.2"); + + // Assert + Assert.True(tcs.Task.Wait(TimeSpan.FromSeconds(30)), + $"Change event was not raised - current time: {DateTime.UtcNow:O}, file LastWriteTimeUtc: {File.GetLastWriteTimeUtc(filePath):O}."); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void UsePollingFileWatcher_UseActivePolling_HasChanged_SymbolicLink_TargetNotExists(bool useWildcard) + { + // Arrange + using var rootOfLink = new DisposableFileSystem(); + string linkName = Path.GetRandomFileName(); + string linkPath = Path.Combine(rootOfLink.RootPath, linkName); + File.CreateSymbolicLink(linkPath, "not-existent-file"); + + // Act + using var provider = new PhysicalFileProvider(rootOfLink.RootPath) { UsePollingFileWatcher = true, UseActivePolling = true }; + IChangeToken token = provider.Watch(useWildcard ? "*" : linkName); + + var tcs = new TaskCompletionSource(); + token.RegisterChangeCallback(_ => { tcs.TrySetResult(); }, null); + + // Assert + Assert.False(tcs.Task.Wait(TimeSpan.FromSeconds(30)), + "Change event was raised when it was not expected."); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task UsePollingFileWatcher_UseActivePolling_HasChanged_SymbolicLink_TargetChanged(bool useWildcard, bool linkWasBroken) + { + // Arrange + using var rootOfFile = new DisposableFileSystem(); + // Create file 2 first as we want to verify that the change is reported regardless of the timestamp being older. + string file2Path = Path.Combine(rootOfFile.RootPath, Path.GetRandomFileName()); + File.WriteAllText(file2Path, "v2.1"); + + string file1Path = Path.Combine(rootOfFile.RootPath, Path.GetRandomFileName()); + if (!linkWasBroken) + { + await Task.Delay(1000); // Wait a second before writing again, see https://github.com/dotnet/runtime/issues/55951. + File.WriteAllText(file1Path, "v1.1"); + } + + using var rootOfLink = new DisposableFileSystem(); + string linkName = Path.GetRandomFileName(); + string linkPath = Path.Combine(rootOfLink.RootPath, linkName); + File.CreateSymbolicLink(linkPath, file1Path); + + string filter = useWildcard ? "*" : linkName; + using var provider = new PhysicalFileProvider(rootOfLink.RootPath) { UsePollingFileWatcher = true, UseActivePolling = true }; + IChangeToken token = provider.Watch(filter); + + var tcs = new TaskCompletionSource(); + token.RegisterChangeCallback(_ => { tcs.TrySetResult(); }, null); + + // Act - Change link target to file 2. + File.Delete(linkPath); + File.CreateSymbolicLink(linkPath, file2Path); + + // Assert - It should report the change regardless of the timestamp being older. + Assert.True(tcs.Task.Wait(TimeSpan.FromSeconds(30)), + $"Change event was not raised - current time: {DateTime.UtcNow:O}, file1 LastWriteTimeUtc: {File.GetLastWriteTimeUtc(file1Path):O}, file2 LastWriteTime: {File.GetLastWriteTimeUtc(file2Path):O}."); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void UsePollingFileWatcher_UseActivePolling_HasChanged_SymbolicLink_TargetDeleted(bool useWildcard) + { + // Arrange + using var rootOfFile = new DisposableFileSystem(); + + string filePath = Path.Combine(rootOfFile.RootPath, Path.GetRandomFileName()); + File.WriteAllText(filePath, "v1.1"); + + using var rootOfLink = new DisposableFileSystem(); + string linkName = Path.GetRandomFileName(); + string linkPath = Path.Combine(rootOfLink.RootPath, linkName); + File.CreateSymbolicLink(linkPath, filePath); + + string filter = useWildcard ? "*" : linkName; + using var provider = new PhysicalFileProvider(rootOfLink.RootPath) { UsePollingFileWatcher = true, UseActivePolling = true }; + IChangeToken token = provider.Watch(filter); + + var tcs = new TaskCompletionSource(); + token.RegisterChangeCallback(_ => { tcs.TrySetResult(); }, null); + + // Act + File.Delete(linkPath); + + // Assert + Assert.True(tcs.Task.Wait(TimeSpan.FromSeconds(30)), + $"Change event was not raised - current time: {DateTime.UtcNow:O}, file LastWriteTimeUtc: {File.GetLastWriteTimeUtc(filePath):O}."); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.FileSystemGlobbing/ref/Microsoft.Extensions.FileSystemGlobbing.csproj b/src/libraries/Microsoft.Extensions.FileSystemGlobbing/ref/Microsoft.Extensions.FileSystemGlobbing.csproj index b939996a35bcf8..ade27836c9d808 100644 --- a/src/libraries/Microsoft.Extensions.FileSystemGlobbing/ref/Microsoft.Extensions.FileSystemGlobbing.csproj +++ b/src/libraries/Microsoft.Extensions.FileSystemGlobbing/ref/Microsoft.Extensions.FileSystemGlobbing.csproj @@ -1,8 +1,12 @@ - netstandard2.0;net461 + $(NetCoreAppCurrent);netstandard2.0;net461 + + + + diff --git a/src/libraries/Microsoft.Extensions.FileSystemGlobbing/src/Microsoft.Extensions.FileSystemGlobbing.csproj b/src/libraries/Microsoft.Extensions.FileSystemGlobbing/src/Microsoft.Extensions.FileSystemGlobbing.csproj index 3b3239f39e12b6..2b6f76bd26c4fb 100644 --- a/src/libraries/Microsoft.Extensions.FileSystemGlobbing/src/Microsoft.Extensions.FileSystemGlobbing.csproj +++ b/src/libraries/Microsoft.Extensions.FileSystemGlobbing/src/Microsoft.Extensions.FileSystemGlobbing.csproj @@ -1,7 +1,7 @@ - + - netstandard2.0;net461 + $(NetCoreAppCurrent);netstandard2.0;net461 true File system globbing to find files matching a specified pattern. @@ -10,5 +10,10 @@ - + + + + + +