Skip to content

Commit

Permalink
Merge pull request #85 from GerardSmit/feature/symlink
Browse files Browse the repository at this point in the history
Add symlink support
  • Loading branch information
xoofx committed Jun 1, 2024
2 parents 51cf2a1 + 52fadd5 commit e243083
Show file tree
Hide file tree
Showing 15 changed files with 640 additions and 6 deletions.
10 changes: 10 additions & 0 deletions src/Zio.Tests/FileSystems/FileSystemEntryRedirect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ protected override void SetLastWriteTimeImpl(UPath path, DateTime time)
_fs.GetFileSystemEntry(path).LastWriteTime = time;
}

protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget)
{
_fs.CreateSymbolicLink(path, pathToTarget);
}

protected override bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath)
{
return _fs.TryResolveLinkTarget(linkPath, out resolvedPath);
}

protected override IEnumerable<UPath> EnumeratePathsImpl(UPath path, string searchPattern, SearchOption searchOption, SearchTarget searchTarget)
{
return _fs.GetDirectoryEntry(path).EnumerateEntries(searchPattern, searchOption, searchTarget).Select(e => e.Path);
Expand Down
70 changes: 69 additions & 1 deletion src/Zio.Tests/FileSystems/TestMountFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
using System.Collections;
using System.IO;
using System.Reflection;

using System.Security.Principal;
using System.Text;
using Zio.FileSystems;

namespace Zio.Tests.FileSystems;
Expand Down Expand Up @@ -427,4 +428,71 @@ public void CopyAndMoveFileCross()
Assert.True(memfs2.FileExists("/file1.txt"));
Assert.Equal("content1", memfs2.ReadAllText("/file1.txt"));
}

[SkippableFact]
public void TestDirectorySymlink()
{
#if NETCOREAPP
if (OperatingSystem.IsWindows())
#else
if (IsWindows)
#endif
{
using var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);

Skip.IfNot(principal.IsInRole(WindowsBuiltInRole.Administrator), "This test requires to be run as an administrator on Windows");
}

var physicalFs = new PhysicalFileSystem();
var memoryFs = new MemoryFileSystem();
var fs = new MountFileSystem();
fs.Mount("/physical", physicalFs);
fs.Mount("/memory", memoryFs);

var pathInfo = physicalFs.ConvertPathFromInternal(SystemPath).ToRelative();
var pathSource = "/physical" / pathInfo / "Source";
var filePathSource = pathSource / "test.txt";
var systemPathSource = Path.Combine(SystemPath, "Source");
var pathDest = "/physical" / pathInfo / "Dest";
var filePathDest = pathDest / "test.txt";
var systemPathDest = Path.Combine(SystemPath, "Dest");

try
{
// CreateDirectory
Assert.False(Directory.Exists(systemPathSource));
fs.CreateDirectory(pathSource);
Assert.True(Directory.Exists(systemPathSource));

// CreateFile / OpenFile
var fileStream = fs.CreateFile(filePathSource);
var buffer = Encoding.UTF8.GetBytes("This is a test");
fileStream.Write(buffer, 0, buffer.Length);
fileStream.Dispose();
Assert.Equal(buffer.Length, fs.GetFileLength(filePathSource));

// CreateSymbolicLink
fs.CreateSymbolicLink(pathDest, pathSource);
Assert.Throws<InvalidOperationException>(() => fs.CreateSymbolicLink("/memory/invalid", pathSource));

// ResolveSymbolicLink
Assert.True(fs.TryResolveLinkTarget(pathDest, out var resolvedPath));
Assert.Equal(pathSource, resolvedPath);

// FileExists
Assert.True(fs.FileExists(filePathDest));
Assert.Equal(buffer.Length, fs.GetFileLength(filePathDest));

// RemoveDirectory
fs.DeleteDirectory(pathDest, false);
Assert.False(Directory.Exists(systemPathDest));
Assert.True(Directory.Exists(systemPathSource));
}
finally
{
SafeDeleteDirectory(systemPathSource);
SafeDeleteDirectory(systemPathDest);
}
}
}
125 changes: 125 additions & 0 deletions src/Zio.Tests/FileSystems/TestPhysicalFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See the license.txt file in the project root for more information.

using System.IO;
using System.Security.Principal;
using System.Text;

using Zio.FileSystems;
Expand Down Expand Up @@ -427,4 +428,128 @@ public void TestFileWindowsExceptions()
SafeDeleteFile(systemFilePath);
}
}

[SkippableFact]
public void TestDirectorySymlink()
{
#if NETCOREAPP
if (OperatingSystem.IsWindows())
#else
if (IsWindows)
#endif
{
using var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);

Skip.IfNot(principal.IsInRole(WindowsBuiltInRole.Administrator), "This test requires to be run as an administrator on Windows");
}

var fs = new PhysicalFileSystem();
var pathInfo = fs.ConvertPathFromInternal(SystemPath);
var pathSource = pathInfo / "Source";
var filePathSource = pathSource / "test.txt";
var systemPathSource = fs.ConvertPathToInternal(pathSource);
var pathDest = pathInfo / "Dest";
var filePathDest = pathDest / "test.txt";
var systemPathDest = fs.ConvertPathToInternal(pathDest);
try
{
// CreateDirectory
Assert.False(Directory.Exists(systemPathSource));
fs.CreateDirectory(pathSource);
Assert.True(Directory.Exists(systemPathSource));

// CreateFile / OpenFile
var fileStream = fs.CreateFile(filePathSource);
var buffer = Encoding.UTF8.GetBytes("This is a test");
fileStream.Write(buffer, 0, buffer.Length);
fileStream.Dispose();
Assert.Equal(buffer.Length, fs.GetFileLength(filePathSource));

// CreateSymbolicLink
fs.CreateSymbolicLink(pathDest, pathSource);

// ResolveSymbolicLink
Assert.True(fs.TryResolveLinkTarget(pathDest, out var resolvedPath));
Assert.Equal(pathSource, resolvedPath);

// FileExists
Assert.True(fs.FileExists(filePathDest));
Assert.Equal(buffer.Length, fs.GetFileLength(filePathDest));

// RemoveDirectory
fs.DeleteDirectory(pathDest, false);
Assert.False(Directory.Exists(systemPathDest));
Assert.True(Directory.Exists(systemPathSource));
}
finally
{
SafeDeleteDirectory(systemPathSource);
SafeDeleteDirectory(systemPathDest);
}
}

[SkippableFact]
public void TestFileSymlink()
{
#if NETCOREAPP
if (OperatingSystem.IsWindows())
#else
if (IsWindows)
#endif
{
using var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);

Skip.IfNot(principal.IsInRole(WindowsBuiltInRole.Administrator), "This test requires to be run as an administrator on Windows");
}

var fs = new PhysicalFileSystem();
var pathInfo = fs.ConvertPathFromInternal(SystemPath);
var pathSource = pathInfo / "source.txt";
var systemPathSource = fs.ConvertPathToInternal(pathSource);
var pathDest = pathInfo / "dest.txt";
var systemPathDest = fs.ConvertPathToInternal(pathDest);
try
{
// CreateEmptyFile
fs.CreateFile(pathSource).Dispose();

// CreateSymbolicLink
fs.CreateSymbolicLink(pathDest, pathSource);

// ResolveSymbolicLink
Assert.True(fs.TryResolveLinkTarget(pathDest, out var resolvedPath));
Assert.Equal(pathSource, resolvedPath);

// FileExists
Assert.True(fs.FileExists(pathDest));

// CreateFile / OpenFile
var fileStream = fs.OpenFile(pathSource, FileMode.Open, FileAccess.ReadWrite);
var buffer = Encoding.UTF8.GetBytes("This is a test");
fileStream.Write(buffer, 0, buffer.Length);
fileStream.Dispose();
Assert.Equal(buffer.Length, fs.GetFileLength(pathSource));

// ReadAllBytes
// Note: we can't check the length, since on Windows the symlink length is 0
var symlinkBuffer = fs.ReadAllBytes(pathDest);
Assert.Equal(buffer, symlinkBuffer);

// FileEntry
var entry = fs.GetFileSystemEntry(pathDest);
Assert.True(entry.Attributes.HasFlag(FileAttributes.ReparsePoint));

// DeleteFile
fs.DeleteFile(pathDest);
Assert.False(File.Exists(systemPathDest));
Assert.True(File.Exists(systemPathSource));
}
finally
{
SafeDeleteFile(systemPathSource);
SafeDeleteFile(systemPathDest);
}
}
}
65 changes: 64 additions & 1 deletion src/Zio.Tests/FileSystems/TestSubFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
// See the license.txt file in the project root for more information.

using System.IO;

using System.Security.Principal;
using System.Text;
using Zio.FileSystems;

namespace Zio.Tests.FileSystems;
Expand Down Expand Up @@ -72,4 +73,66 @@ public void TestWatcher()
System.Threading.Thread.Sleep(100);
Assert.True(gotChange);
}

[SkippableFact]
public void TestDirectorySymlink()
{
#if NETCOREAPP
if (OperatingSystem.IsWindows())
#else
if (IsWindows)
#endif
{
using var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);

Skip.IfNot(principal.IsInRole(WindowsBuiltInRole.Administrator), "This test requires to be run as an administrator on Windows");
}

var physicalFs = new PhysicalFileSystem();
var fs = new SubFileSystem(physicalFs, physicalFs.ConvertPathFromInternal(SystemPath));

UPath pathSource = "/Source";
var filePathSource = pathSource / "test.txt";
var systemPathSource = fs.ConvertPathToInternal(pathSource);
UPath pathDest = "/Dest";
var filePathDest = pathDest / "test.txt";
var systemPathDest = fs.ConvertPathToInternal(pathDest);

try
{
// CreateDirectory
Assert.False(Directory.Exists(systemPathSource));
fs.CreateDirectory(pathSource);
Assert.True(Directory.Exists(systemPathSource));

// CreateFile / OpenFile
var fileStream = fs.CreateFile(filePathSource);
var buffer = Encoding.UTF8.GetBytes("This is a test");
fileStream.Write(buffer, 0, buffer.Length);
fileStream.Dispose();
Assert.Equal(buffer.Length, fs.GetFileLength(filePathSource));

// CreateSymbolicLink
fs.CreateSymbolicLink(pathDest, pathSource);

// ResolveSymbolicLink
Assert.True(fs.TryResolveLinkTarget(pathDest, out var resolvedPath));
Assert.Equal(pathSource, resolvedPath);

// FileExists
Assert.True(fs.FileExists(filePathDest));
Assert.Equal(buffer.Length, fs.GetFileLength(filePathDest));

// RemoveDirectory
fs.DeleteDirectory(pathDest, false);
Assert.False(Directory.Exists(systemPathDest));
Assert.True(Directory.Exists(systemPathSource));
}
finally
{
SafeDeleteDirectory(systemPathSource);
SafeDeleteDirectory(systemPathDest);
}
}
}
4 changes: 2 additions & 2 deletions src/Zio/FileEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ public void WriteAllText(string content, Encoding encoding)
/// <remarks>
/// Given a string and a file path, this method opens the specified file, appends the string to the end of the file,
/// and then closes the file. The file handle is guaranteed to be closed by this method, even if exceptions are raised.
/// The method creates the file if it doesn’t exist, but it doesn't create new directories. Therefore, the value of the
/// The method creates the file if it doesn't exist, but it doesn't create new directories. Therefore, the value of the
/// path parameter must contain existing directories.
/// </remarks>
public void AppendAllText(string content)
Expand All @@ -301,7 +301,7 @@ public void AppendAllText(string content)
/// <remarks>
/// Given a string and a file path, this method opens the specified file, appends the string to the end of the file,
/// and then closes the file. The file handle is guaranteed to be closed by this method, even if exceptions are raised.
/// The method creates the file if it doesn’t exist, but it doesn't create new directories. Therefore, the value of the
/// The method creates the file if it doesn't exist, but it doesn't create new directories. Therefore, the value of the
/// path parameter must contain existing directories.
/// </remarks>
public void AppendAllText(string content, Encoding encoding)
Expand Down
4 changes: 2 additions & 2 deletions src/Zio/FileSystemExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ public static void WriteAllText(this IFileSystem fs, UPath path, string content,
/// <remarks>
/// Given a string and a file path, this method opens the specified file, appends the string to the end of the file,
/// and then closes the file. The file handle is guaranteed to be closed by this method, even if exceptions are raised.
/// The method creates the file if it doesnt exist, but it doesn't create new directories. Therefore, the value of the
/// The method creates the file if it doesn't exist, but it doesn't create new directories. Therefore, the value of the
/// path parameter must contain existing directories.
/// </remarks>
public static void AppendAllText(this IFileSystem fs, UPath path, string content)
Expand All @@ -531,7 +531,7 @@ public static void AppendAllText(this IFileSystem fs, UPath path, string content
/// <remarks>
/// Given a string and a file path, this method opens the specified file, appends the string to the end of the file,
/// and then closes the file. The file handle is guaranteed to be closed by this method, even if exceptions are raised.
/// The method creates the file if it doesnt exist, but it doesn't create new directories. Therefore, the value of the
/// The method creates the file if it doesn't exist, but it doesn't create new directories. Therefore, the value of the
/// path parameter must contain existing directories.
/// </remarks>
public static void AppendAllText(this IFileSystem fs, UPath path, string content, Encoding encoding)
Expand Down
19 changes: 19 additions & 0 deletions src/Zio/FileSystems/ComposeFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,25 @@ protected override void SetLastWriteTimeImpl(UPath path, DateTime time)
FallbackSafe.SetLastWriteTime(ConvertPathToDelegate(path), time);
}

/// <inheritdoc />
protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget)
{
FallbackSafe.CreateSymbolicLink(ConvertPathToDelegate(path), ConvertPathToDelegate(pathToTarget));
}

/// <inheritdoc />
protected override bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath)
{
if (!FallbackSafe.TryResolveLinkTarget(ConvertPathToDelegate(linkPath), out var resolvedPathDelegate))
{
resolvedPath = default;
return false;
}

resolvedPath = ConvertPathFromDelegate(resolvedPathDelegate);
return true;
}

// ----------------------------------------------
// Search API
// ----------------------------------------------
Expand Down
Loading

0 comments on commit e243083

Please sign in to comment.