<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
+ <TargetFrameworks>$(NetCoreAppCurrent);netstandard2.0;net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Compile Include="Microsoft.Extensions.FileProviders.Abstractions.cs" />
<ItemGroup>
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Primitives\ref\Microsoft.Extensions.Primitives.csproj" />
</ItemGroup>
+ <ItemGroup Condition="'$(TargetFramework)' == '$(NetCoreAppCurrent)'">
+ <ProjectReference Include="$(LibrariesProjectRoot)System.Runtime\ref\System.Runtime.csproj" />
+ </ItemGroup>
</Project>
<PropertyGroup>
<RootNamespace>Microsoft.Extensions.FileProviders</RootNamespace>
- <TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
+ <TargetFrameworks>$(NetCoreAppCurrent);netstandard2.0;net461</TargetFrameworks>
<EnableDefaultItems>true</EnableDefaultItems>
<PackageDescription>Abstractions of files and directories.
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Primitives\src\Microsoft.Extensions.Primitives.csproj" />
</ItemGroup>
+ <ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
+ <Reference Include="System.Runtime" />
+ <Reference Include="System.Linq" />
+ <Reference Include="System.Linq.Expressions" />
+ </ItemGroup>
</Project>
<Import Project="..\Directory.Build.props" />
<PropertyGroup>
<IsAspNetCoreApp>true</IsAspNetCoreApp>
+ <UnsupportedOSPlatforms>browser</UnsupportedOSPlatforms>
</PropertyGroup>
</Project>
\ No newline at end of file
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
+ <TargetFrameworks>$(NetCoreAppCurrent);netstandard2.0;net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Compile Include="Microsoft.Extensions.FileProviders.Physical.cs" />
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.FileSystemGlobbing\ref\Microsoft.Extensions.FileSystemGlobbing.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Primitives\ref\Microsoft.Extensions.Primitives.csproj" />
</ItemGroup>
+ <ItemGroup Condition="'$(TargetFramework)' == '$(NetCoreAppCurrent)'">
+ <ProjectReference Include="$(LibrariesProjectRoot)System.IO.FileSystem.Watcher\ref\System.IO.FileSystem.Watcher.csproj" />
+ </ItemGroup>
</Project>
// 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
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;
+ }
}
}
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Microsoft.Extensions.FileProviders</RootNamespace>
- <TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
+ <TargetFrameworks>$(NetCoreAppCurrent);$(NetCoreAppCurrent)-Browser;netstandard2.0;net461</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnableDefaultItems>true</EnableDefaultItems>
<PackageDescription>File provider for physical files for Microsoft.Extensions.FileProviders.</PackageDescription>
</PropertyGroup>
+
+ <PropertyGroup Condition="'$(TargetsBrowser)' == 'true'">
+ <EnableDefaultItems>false</EnableDefaultItems>
+ <GeneratePlatformNotSupportedAssemblyMessage>SR.FileProvidersPhysical_PlatformNotSupported</GeneratePlatformNotSupportedAssemblyMessage>
+ </PropertyGroup>
<ItemGroup>
<Compile Include="$(CommonPath)Extensions\EmptyDisposable.cs"
<PackageReference Include="System.Security.Cryptography.Algorithms" Version="$(SystemSecurityCryptographyAlgorithmsVersion)" />
</ItemGroup>
+ <ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
+ <Reference Include="System.Collections.Concurrent" />
+ <Reference Include="System.ComponentModel.Primitives" />
+ <Reference Include="System.IO.FileSystem.Watcher" />
+ <Reference Include="System.Linq" />
+ <Reference Include="System.Linq.Expressions" />
+ <Reference Include="System.Runtime" />
+ <Reference Include="System.Security.Cryptography.Algorithms" />
+ <Reference Include="System.Security.Cryptography.Primitives" />
+ <Reference Include="System.Threading" />
+ </ItemGroup>
</Project>
private DateTime GetLastWriteTimeUtc()
{
_fileInfo.Refresh();
- return _fileInfo.Exists ? _fileInfo.LastWriteTimeUtc : DateTime.MinValue;
+
+ if (!_fileInfo.Exists)
+ {
+ return DateTime.MinValue;
+ }
+
+ return FileSystemInfoHelper.GetFileLinkTargetLastWriteTimeUtc(_fileInfo) ?? _fileInfo.LastWriteTimeUtc;
}
/// <summary>
/// <returns>The <see cref="DateTime"/> that the file was last modified.</returns>
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)
<data name="UnexpectedFileSystemInfo" xml:space="preserve">
<value>Unexpected type of FileSystemInfo</value>
</data>
+ <data name="FileProvidersPhysical_PlatformNotSupported" xml:space="preserve">
+ <value>Microsoft.Extensions.FileProviders.Physical is not supported on this platform.</value>
+ </data>
</root>
\ No newline at end of file
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Microsoft.Extensions.FileProviders.Physical</RootNamespace>
<TargetFrameworks>$(NetCoreAppCurrent);net461</TargetFrameworks>
<EnableDefaultItems>true</EnableDefaultItems>
<IgnoreForCI Condition="'$(TargetOS)' == 'Browser'">true</IgnoreForCI>
+ <IncludePlatformAttributes>false</IncludePlatformAttributes>
</PropertyGroup>
<ItemGroup>
Link="Common\System\Threading\Tasks\TaskTimeoutExtensions.cs" />
</ItemGroup>
+ <ItemGroup Condition="'$(TargetFramework)' != '$(NetCoreAppCurrent)'">
+ <Compile Remove="PhysicalFileProviderTests.netcoreapp.cs" />
+ </ItemGroup>
+
<ItemGroup>
<PackageReference Include="Moq" Version="$(MoqVersion)" />
<ProjectReference Include="..\src\Microsoft.Extensions.FileProviders.Physical.csproj" SkipUseReferenceAssembly="true" />
namespace Microsoft.Extensions.FileProviders
{
- public class PhysicalFileProviderTests
+ public partial class PhysicalFileProviderTests
{
private const int WaitTimeForTokenToFire = 500;
private const int WaitTimeForTokenCallback = 10000;
}
}
+ [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<object>();
+ 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<object>();
+ 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()
{
--- /dev/null
+// 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}.");
+ }
+ }
+}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
+ <TargetFrameworks>$(NetCoreAppCurrent);netstandard2.0;net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Compile Include="Microsoft.Extensions.FileSystemGlobbing.cs" />
</ItemGroup>
+ <ItemGroup Condition="'$(TargetFramework)' == '$(NetCoreAppCurrent)'">
+ <ProjectReference Include="$(LibrariesProjectRoot)System.Collections\ref\System.Collections.csproj" />
+ <ProjectReference Include="$(LibrariesProjectRoot)System.Runtime\ref\System.Runtime.csproj" />
+ </ItemGroup>
</Project>
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
+ <TargetFrameworks>$(NetCoreAppCurrent);netstandard2.0;net461</TargetFrameworks>
<EnableDefaultItems>true</EnableDefaultItems>
<PackageDescription>File system globbing to find files matching a specified pattern.</PackageDescription>
</PropertyGroup>
<Compile Include="$(CoreLibSharedDir)\System\Numerics\Hashing\HashHelpers.cs"
Link="System\Numerics\Hashing\HashHelpers.cs" />
</ItemGroup>
-
+
+ <ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
+ <Reference Include="System.Collections" />
+ <Reference Include="System.Linq" />
+ <Reference Include="System.Runtime" />
+ </ItemGroup>
</Project>