Skip to content

Commit

Permalink
feat: add CreateSymbolicLink support (#871)
Browse files Browse the repository at this point in the history
* Adds CreateSymbolicLink support

Adds a FileSystemInfo extension for wrapping convenience.
Adds FEATURE_CREATE_SYMBOLIC_LINK to the build properties.
Includes mock implementations and mock tests.
Updates the ApiParityTests to no longer ignore CreateSymbolicLink for .net 6.0.
Bumps the version in version.json.

* Skip path tests on non-Windows platform

Co-authored-by: Florian Greinacher <[email protected]>
  • Loading branch information
davidrogers090 and fgreinacher authored Aug 15, 2022
1 parent df30ba4 commit defd44b
Show file tree
Hide file tree
Showing 15 changed files with 541 additions and 7 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<DefineConstants Condition="'$(TargetFramework)' != 'net461'">$(DefineConstants);FEATURE_FILE_SYSTEM_ACL_EXTENSIONS</DefineConstants>
<DefineConstants Condition="'$(TargetFramework)' == 'net6.0' OR '$(TargetFramework)' == 'net5.0' OR '$(TargetFramework)' == 'netcoreapp3.1' OR '$(TargetFramework)' == 'netstandard2.1'">$(DefineConstants);FEATURE_ASYNC_FILE;FEATURE_ENUMERATION_OPTIONS;FEATURE_ADVANCED_PATH_OPERATIONS;FEATURE_PATH_JOIN_WITH_SPAN</DefineConstants>
<DefineConstants Condition="'$(TargetFramework)' == 'net6.0' OR '$(TargetFramework)' == 'net5.0'">$(DefineConstants);FEATURE_FILE_MOVE_WITH_OVERWRITE;FEATURE_SUPPORTED_OS_ATTRIBUTE;FEATURE_FILE_SYSTEM_WATCHER_FILTERS;FEATURE_ENDS_IN_DIRECTORY_SEPARATOR;FEATURE_PATH_JOIN_WITH_PARAMS;FEATURE_PATH_JOIN_WITH_FOUR_PATHS</DefineConstants>
<DefineConstants Condition="'$(TargetFramework)' == 'net6.0'">$(DefineConstants);FEATURE_FILE_SYSTEM_INFO_LINK_TARGET</DefineConstants>
<DefineConstants Condition="'$(TargetFramework)' == 'net6.0'">$(DefineConstants);FEATURE_FILE_SYSTEM_INFO_LINK_TARGET;FEATURE_CREATE_SYMBOLIC_LINK</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" Version="3.5.109">
Expand Down
24 changes: 24 additions & 0 deletions src/System.IO.Abstractions.TestingHelpers/MockDirectory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,30 @@ private IDirectoryInfo CreateDirectoryInternal(string path, DirectorySecurity di
return created;
}

#if FEATURE_CREATE_SYMBOLIC_LINK
/// <inheritdoc />
public override IFileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
{
mockFileDataAccessor.PathVerifier.IsLegalAbsoluteOrRelative(path, nameof(path));
mockFileDataAccessor.PathVerifier.IsLegalAbsoluteOrRelative(pathToTarget, nameof(pathToTarget));

if (Exists(path))
{
throw CommonExceptions.FileAlreadyExists(nameof(path));
}

var targetExists = Exists(pathToTarget);
if (!targetExists)
{
throw CommonExceptions.FileNotFound(pathToTarget);
}

mockFileDataAccessor.AddDirectory(path);
mockFileDataAccessor.GetFile(path).LinkTarget = pathToTarget;

return new MockDirectoryInfo(mockFileDataAccessor, path);
}
#endif

/// <inheritdoc />
public override void Delete(string path)
Expand Down
40 changes: 40 additions & 0 deletions src/System.IO.Abstractions.TestingHelpers/MockFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,46 @@ private Stream CreateInternal(string path, FileAccess access, FileOptions option
return OpenInternal(path, FileMode.Open, access, options);
}

#if FEATURE_CREATE_SYMBOLIC_LINK
/// <inheritdoc />
public override IFileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
{
if (path == null)
{
throw CommonExceptions.FilenameCannotBeNull(nameof(path));
}

if (pathToTarget == null)
{
throw CommonExceptions.FilenameCannotBeNull(nameof(pathToTarget));
}

mockFileDataAccessor.PathVerifier.IsLegalAbsoluteOrRelative(path, nameof(path));
mockFileDataAccessor.PathVerifier.IsLegalAbsoluteOrRelative(pathToTarget, nameof(pathToTarget));

if (Exists(path))
{
throw CommonExceptions.FileAlreadyExists(nameof(path));
}

VerifyDirectoryExists(path);

var fileExists = mockFileDataAccessor.FileExists(pathToTarget);
if (!fileExists)
{
throw CommonExceptions.FileNotFound(pathToTarget);
}

var sourceFileData = mockFileDataAccessor.GetFile(pathToTarget);
sourceFileData.CheckFileAccess(pathToTarget, FileAccess.Read);
var destFileData = new MockFileData(new byte[0]);
destFileData.CreationTime = destFileData.LastAccessTime = DateTime.Now;
destFileData.LinkTarget = pathToTarget;
mockFileDataAccessor.AddFile(path, destFileData);

return new MockFileInfo(mockFileDataAccessor, path);
}
#endif
/// <inheritdoc />
public override StreamWriter CreateText(string path)
{
Expand Down
3 changes: 3 additions & 0 deletions src/System.IO.Abstractions/Converters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ internal static IEnumerable<FileSystemInfoBase> WrapFileSystemInfos(this IEnumer
internal static FileSystemInfoBase[] WrapFileSystemInfos(this FileSystemInfo[] input, IFileSystem fileSystem)
=> input.Select(info => WrapFileSystemInfo(fileSystem, info)).ToArray();

internal static FileSystemInfoBase WrapFileSystemInfo(this FileSystemInfo input, IFileSystem fileSystem)
=> WrapFileSystemInfo(fileSystem, input);

internal static IEnumerable<DirectoryInfoBase> WrapDirectories(this IEnumerable<DirectoryInfo> input, IFileSystem fileSystem)
=> input.Select(info => WrapDirectoryInfo(fileSystem, info));

Expand Down
5 changes: 4 additions & 1 deletion src/System.IO.Abstractions/DirectoryBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ internal DirectoryBase() { }
/// <inheritdoc cref="IDirectory.CreateDirectory(string,DirectorySecurity)"/>
[SupportedOSPlatform("windows")]
public abstract IDirectoryInfo CreateDirectory(string path, DirectorySecurity directorySecurity);

#if FEATURE_CREATE_SYMBOLIC_LINK
/// <inheritdoc cref="IDirectory.CreateSymbolicLink(string, string)"/>
public abstract IFileSystemInfo CreateSymbolicLink(string path, string pathToTarget);
#endif
/// <inheritdoc cref="IDirectory.Delete(string)"/>
public abstract void Delete(string path);

Expand Down
8 changes: 7 additions & 1 deletion src/System.IO.Abstractions/DirectoryWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ public override IDirectoryInfo CreateDirectory(string path, DirectorySecurity di
directoryInfo.Create(directorySecurity);
return new DirectoryInfoWrapper(FileSystem, directoryInfo);
}

#if FEATURE_CREATE_SYMBOLIC_LINK
/// <inheritdoc />
public override IFileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
{
return Directory.CreateSymbolicLink(path, pathToTarget).WrapFileSystemInfo(FileSystem);
}
#endif
/// <inheritdoc />
public override void Delete(string path)
{
Expand Down
5 changes: 4 additions & 1 deletion src/System.IO.Abstractions/FileBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ internal FileBase() { }

/// <inheritdoc cref="IFile.Create(string,int,FileOptions)"/>
public abstract Stream Create(string path, int bufferSize, FileOptions options);

#if FEATURE_CREATE_SYMBOLIC_LINK
/// <inheritdoc cref="IFile.CreateSymbolicLink(string, string)"/>
public abstract IFileSystemInfo CreateSymbolicLink(string path, string pathToTarget);
#endif
/// <inheritdoc cref="IFile.CreateText"/>
public abstract StreamWriter CreateText(string path);
/// <inheritdoc cref="IFile.Decrypt"/>
Expand Down
7 changes: 7 additions & 0 deletions src/System.IO.Abstractions/FileWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ public override Stream Create(string path, int bufferSize, FileOptions options)
return File.Create(path, bufferSize, options);
}

#if FEATURE_CREATE_SYMBOLIC_LINK
/// <inheritdoc />
public override IFileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
{
return File.CreateSymbolicLink(path, pathToTarget).WrapFileSystemInfo(FileSystem);
}
#endif
/// <inheritdoc />
public override StreamWriter CreateText(string path)
{
Expand Down
4 changes: 4 additions & 0 deletions src/System.IO.Abstractions/IDirectory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public interface IDirectory
/// <inheritdoc cref="Directory.CreateDirectory(string,DirectorySecurity)"/>
#endif
IDirectoryInfo CreateDirectory(string path, DirectorySecurity directorySecurity);
#if FEATURE_CREATE_SYMBOLIC_LINK
/// <inheritdoc cref="Directory.CreateSymbolicLink"/>
IFileSystemInfo CreateSymbolicLink(string path, string pathToTarget);
#endif
/// <inheritdoc cref="Directory.Delete(string)"/>
void Delete(string path);
/// <inheritdoc cref="Directory.Delete(string,bool)"/>
Expand Down
4 changes: 4 additions & 0 deletions src/System.IO.Abstractions/IFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ public partial interface IFile
Stream Create(string path, int bufferSize);
/// <inheritdoc cref="File.Create(string,int,FileOptions)"/>
Stream Create(string path, int bufferSize, FileOptions options);
#if FEATURE_CREATE_SYMBOLIC_LINK
/// <inheritdoc cref="File.CreateSymbolicLink"/>
IFileSystemInfo CreateSymbolicLink(string path, string pathToTarget);
#endif
/// <inheritdoc cref="File.CreateText"/>
StreamWriter CreateText(string path);
/// <inheritdoc cref="File.Decrypt"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Versioning;
using System.Security.AccessControl;
using NUnit.Framework;

namespace System.IO.Abstractions.TestingHelpers.Tests
{
using XFS = MockUnixSupport;

[TestFixture]
public class MockDirectorySymlinkTests
{

#if FEATURE_CREATE_SYMBOLIC_LINK

[Test]
public void MockDirectory_CreateSymbolicLink_ShouldReturnFileSystemInfo()
{
// Arrange
var fileSystem = new MockFileSystem();
var pathToTarget = XFS.Path(@"C:\Folder\foo");
var path = XFS.Path(@"C:\bar");
fileSystem.AddDirectory(pathToTarget);

// Act
IFileSystemInfo fileSystemInfo = fileSystem.Directory.CreateSymbolicLink(path, pathToTarget);

// Assert
Assert.AreEqual(path, fileSystemInfo.FullName);
Assert.AreEqual(pathToTarget, fileSystemInfo.LinkTarget);
}

[Test]
public void MockDirectory_CreateSymbolicLink_ShouldSucceedFromDirectoryInfo()
{
// Arrange
var fileSystem = new MockFileSystem();
var pathToTarget = XFS.Path(@"C:\Folder\foo");
var path = XFS.Path(@"C:\bar");
fileSystem.AddDirectory(pathToTarget);

// Act
fileSystem.Directory.CreateSymbolicLink(path, pathToTarget);
IDirectoryInfo directoryInfo = fileSystem.DirectoryInfo.FromDirectoryName(path);

// Assert
Assert.AreEqual(path, directoryInfo.FullName);
Assert.AreEqual(pathToTarget, directoryInfo.LinkTarget);
}

[Test]
public void MockDirectory_CreateSymbolicLink_ShouldFailWithNullPath()
{
// Arrange
var fileSystem = new MockFileSystem();
var pathToTarget = XFS.Path(@"C:\Folder\foo");
fileSystem.AddDirectory(pathToTarget);

// Act
var ex = Assert.Throws<ArgumentNullException>(() => fileSystem.Directory.CreateSymbolicLink(null, pathToTarget));

// Assert
Assert.That(ex.ParamName, Is.EqualTo("path"));
}

[Test]
public void MockDirectory_CreateSymbolicLink_ShouldFailWithNullTarget()
{
// Arrange
var fileSystem = new MockFileSystem();
var path = XFS.Path(@"C:\Folder\foo");
fileSystem.AddDirectory(path);

// Act
var ex = Assert.Throws<ArgumentNullException>(() => fileSystem.Directory.CreateSymbolicLink(path, null));

// Assert
Assert.That(ex.ParamName, Is.EqualTo("pathToTarget"));
}

[Test]
public void MockDirectory_CreateSymbolicLink_ShouldFailWithEmptyPath()
{
// Arrange
var fileSystem = new MockFileSystem();
var pathToTarget = XFS.Path(@"C:\Folder\foo");
fileSystem.AddDirectory(pathToTarget);

// Act
var ex = Assert.Throws<ArgumentException>(() => fileSystem.Directory.CreateSymbolicLink("", pathToTarget));

// Assert
Assert.That(ex.ParamName, Is.EqualTo("path"));
}

[Test]
public void MockDirectory_CreateSymbolicLink_ShouldFailWithEmptyTarget()
{
// Arrange
var fileSystem = new MockFileSystem();
string path = XFS.Path(@"C:\Folder\foo");
fileSystem.AddDirectory(path);

// Act
var ex = Assert.Throws<ArgumentException>(() => fileSystem.Directory.CreateSymbolicLink(path, ""));

// Assert
Assert.That(ex.ParamName, Is.EqualTo("pathToTarget"));
}

[Test]
public void MockDirectory_CreateSymbolicLink_ShouldFailWithIllegalPath()
{
// Arrange
var fileSystem = new MockFileSystem();
string pathToTarget = XFS.Path(@"C:\Folder\foo");
fileSystem.AddDirectory(pathToTarget);

// Act
var ex = Assert.Throws<ArgumentException>(() => fileSystem.Directory.CreateSymbolicLink(" ", pathToTarget));

// Assert
Assert.That(ex.ParamName, Is.EqualTo("path"));
}

[Test]
public void MockDirectory_CreateSymbolicLink_ShouldFailWithIllegalTarget()
{
// Arrange
var fileSystem = new MockFileSystem();
string path = XFS.Path(@"C:\Folder\foo");
fileSystem.AddDirectory(path);

// Act
var ex = Assert.Throws<ArgumentException>(() => fileSystem.Directory.CreateSymbolicLink(path, " "));

// Assert
Assert.That(ex.ParamName, Is.EqualTo("pathToTarget"));
}

[Test]
[WindowsOnly(WindowsSpecifics.StrictPathRules)]
public void MockDirectory_CreateSymbolicLink_ShouldFailWithIllegalCharactersInPath()
{
// Arrange
var fileSystem = new MockFileSystem();
string pathToTarget = XFS.Path(@"C:\Folder\foo");
fileSystem.AddDirectory(pathToTarget);

// Act
TestDelegate ex = () => fileSystem.Directory.CreateSymbolicLink(@"C:\bar_?_", pathToTarget);

// Assert
Assert.Throws<ArgumentException>(ex);
}

[Test]
[WindowsOnly(WindowsSpecifics.StrictPathRules)]
public void MockDirectory_CreateSymbolicLink_ShouldFailWithIllegalCharactersInTarget()
{
// Arrange
var fileSystem = new MockFileSystem();
string path = XFS.Path(@"C:\foo");

// Act
TestDelegate ex = () => fileSystem.Directory.CreateSymbolicLink(path, @"C:\bar_?_");

// Assert
Assert.Throws<ArgumentException>(ex);
}

[Test]
public void MockDirectory_CreateSymbolicLink_ShouldFailIfPathExists()
{
// Arrange
var fileSystem = new MockFileSystem();
string pathToTarget = XFS.Path(@"C:\Folder\foo");
string path = XFS.Path(@"C:\Folder\bar");
fileSystem.AddDirectory(pathToTarget);
fileSystem.AddDirectory(path);

// Act
var ex = Assert.Throws<IOException>(() => fileSystem.Directory.CreateSymbolicLink(path, pathToTarget));

// Assert
Assert.That(ex.Message.Contains("path"));
}

[Test]
public void MockDirectory_CreateSymbolicLink_ShouldFailIfTargetDoesNotExist()
{
// Arrange
var fileSystem = new MockFileSystem();
string path = XFS.Path(@"C:\Folder\foo");
string pathToTarget = XFS.Path(@"C:\Target");

// Act
var ex = Assert.Throws<FileNotFoundException>(() => fileSystem.Directory.CreateSymbolicLink(path, pathToTarget));

// Assert
Assert.That(ex.Message.Contains(pathToTarget));
}
#endif
}
}
Loading

0 comments on commit defd44b

Please sign in to comment.