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
8 changes: 6 additions & 2 deletions src/Zio.Tests/FileSystems/TestMountFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,6 @@ public void TestMount()
Assert.Throws<ArgumentNullException>(() => fs.Mount("/test", null));
Assert.Throws<ArgumentException>(() => fs.Mount("test", memfs));
Assert.Throws<ArgumentException>(() => fs.Mount("/", memfs));
Assert.Throws<ArgumentException>(() => fs.Mount("/test/a", memfs));
Assert.Throws<ArgumentException>(() => fs.Mount("/test/a/b", memfs));

Assert.False(fs.IsMounted("/test"));
fs.Mount("/test", memfs);
Expand Down Expand Up @@ -139,6 +137,12 @@ public void TestMount()
fs.Unmount("/test2");

Assert.Equal(0, fs.GetMounts().Count);

var innerFs = GetCommonMemoryFileSystem();
fs.Mount("/x/y", innerFs);
fs.Mount("/x/y/b", innerFs);
Assert.True(fs.FileExists("/x/y/A.txt"));
Assert.True(fs.FileExists("/x/y/b/A.txt"));
}


Expand Down
6 changes: 6 additions & 0 deletions src/Zio.Tests/TestUPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,12 @@ public void TestChangeExtension(string path1, string newExt, string expectedPath

// Test relative paths
[InlineData("a/b", "a", false, true)]

// Test exact match
[InlineData("/a/b/", "/a/b/", false, true)]
[InlineData("/a/b/", "/a/b/", true, true)]
[InlineData("/a/b", "/a/b", false, true)]
[InlineData("/a/b", "/a/b", true, true)]
public void TestIsInDirectory(string path1, string directory, bool recursive, bool expected)
{
var path = (UPath)path1;
Expand Down
122 changes: 70 additions & 52 deletions src/Zio/FileSystems/MountFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace Zio.FileSystems
/// </summary>
public class MountFileSystem : ComposeFileSystem
{
private readonly Dictionary<string, IFileSystem> _mounts;
private readonly SortedList<UPath, IFileSystem> _mounts;
private readonly List<AggregateFileSystemWatcher> _aggregateWatchers;
private readonly List<Watcher> _watchers;

Expand All @@ -33,7 +33,7 @@ public MountFileSystem() : this(null)
/// <param name="defaultBackupFileSystem">The default backup file system.</param>
public MountFileSystem(IFileSystem defaultBackupFileSystem) : base(defaultBackupFileSystem)
{
_mounts = new Dictionary<string, IFileSystem>();
_mounts = new SortedList<UPath, IFileSystem>(new UPathLengthComparer());
_aggregateWatchers = new List<AggregateFileSystemWatcher>();
_watchers = new List<Watcher>();
}
Expand All @@ -45,9 +45,9 @@ public MountFileSystem(IFileSystem defaultBackupFileSystem) : base(defaultBackup
/// <param name="fileSystem">The file system.</param>
/// <exception cref="System.ArgumentNullException">fileSystem</exception>
/// <exception cref="System.ArgumentException">
/// Cannot recursively mount the filesystem to self - fileSystem
/// Cannot recursively mount the filesystem to self - <paramref name="fileSystem"/>
/// or
/// There is already a mount with the same name: `{mountName}` - name
/// There is already a mount with the same name: `{name}` - <paramref name="name"/>
/// </exception>
public void Mount(UPath name, IFileSystem fileSystem)
{
Expand All @@ -57,22 +57,22 @@ public void Mount(UPath name, IFileSystem fileSystem)
throw new ArgumentException("Cannot recursively mount the filesystem to self", nameof(fileSystem));
}
AssertMountName(name);
var mountName = name.GetName();
name = NormalizeMountName(name);

lock (_mounts)
{
if (_mounts.ContainsKey(mountName))
if (_mounts.ContainsKey(name))
{
throw new ArgumentException($"There is already a mount with the same name: `{mountName}`", nameof(name));
throw new ArgumentException($"There is already a mount with the same name: `{name}`", nameof(name));
}
_mounts.Add(mountName, fileSystem);
_mounts.Add(name, fileSystem);

lock (_aggregateWatchers)
{
foreach (var watcher in _aggregateWatchers)
{
var internalWatcher = fileSystem.Watch(UPath.Root);
watcher.Add(new Watcher(this, mountName, UPath.Root, internalWatcher));
watcher.Add(new Watcher(this, name, UPath.Root, internalWatcher));
}
}
}
Expand All @@ -86,11 +86,11 @@ public void Mount(UPath name, IFileSystem fileSystem)
public bool IsMounted(UPath name)
{
AssertMountName(name);
var mountName = name.GetName();
name = NormalizeMountName(name);

lock (_mounts)
{
return _mounts.ContainsKey(mountName);
return _mounts.ContainsKey(name);
}
}

Expand All @@ -105,7 +105,7 @@ public Dictionary<UPath, IFileSystem> GetMounts()
{
foreach (var mount in _mounts)
{
dict.Add(UPath.Root / mount.Key, mount.Value);
dict.Add(mount.Key, mount.Value);
}
}
return dict;
Expand All @@ -115,21 +115,22 @@ public Dictionary<UPath, IFileSystem> GetMounts()
/// Unmounts the specified mount name and its attached filesystem.
/// </summary>
/// <param name="name">The mount name.</param>
/// <exception cref="System.ArgumentException">The mount with the name `{mountName}` was not found</exception>
/// <exception cref="System.ArgumentException">The mount with the name <paramref name="name"/> was not found</exception>
public void Unmount(UPath name)
{
AssertMountName(name);
var mountName = name.GetName();
IFileSystem mountFileSystem = null;
name = NormalizeMountName(name);

IFileSystem mountFileSystem;

lock (_mounts)
{
if (!_mounts.TryGetValue(mountName, out mountFileSystem))
if (!_mounts.TryGetValue(name, out mountFileSystem))
{
throw new ArgumentException($"The mount with the name `{mountName}` was not found");
throw new ArgumentException($"The mount with the name `{name}` was not found");
}

_mounts.Remove(mountName);
_mounts.Remove(name);
}

lock (_aggregateWatchers)
Expand Down Expand Up @@ -492,8 +493,7 @@ SortedSet<UPath> GetRootDirectories()
{
foreach (var mountName in _mounts.Keys)
{
var mountPath = UPath.Root / mountName;
directories.Add(mountPath);
directories.Add(mountName);
}
}

Expand All @@ -513,8 +513,7 @@ SortedSet<UPath> GetRootDirectories()

IEnumerable<UPath> EnumeratePathFromFileSystem(UPath subPath, bool failOnInvalidPath)
{
string mountName;
var fs = TryGetMountOrNext(ref subPath, out mountName);
var fs = TryGetMountOrNext(ref subPath, out var mountPath);

if (fs == null)
{
Expand All @@ -528,11 +527,11 @@ IEnumerable<UPath> EnumeratePathFromFileSystem(UPath subPath, bool failOnInvalid
if (fs != NextFileSystem)
{
// In the case of a mount, we need to return the full path
Debug.Assert(mountName != null);
var pathPrefix = UPath.Root / mountName;
Debug.Assert(!mountPath.IsNull);

foreach (var entry in fs.EnumeratePaths(subPath, searchPattern, searchOption, searchTarget))
{
yield return pathPrefix / entry.ToRelative();
yield return mountPath / entry.ToRelative();
}
}
else
Expand Down Expand Up @@ -633,14 +632,14 @@ protected override IFileSystemWatcher WatchImpl(UPath path)
{
// watch only one mount point
var internalPath = path;
var fs = TryGetMountOrNext(ref internalPath, out var mountName);
var fs = TryGetMountOrNext(ref internalPath, out var mountPath);
if (fs == null)
{
throw NewFileNotFoundException(path);
}

var internalWatcher = fs.Watch(internalPath);
var watcher = new Watcher(this, mountName, path, internalWatcher);
var watcher = new Watcher(this, mountPath, path, internalWatcher);

lock (_watchers)
{
Expand All @@ -653,23 +652,23 @@ protected override IFileSystemWatcher WatchImpl(UPath path)

private class Watcher : WrapFileSystemWatcher
{
private readonly string _mountName;
private readonly UPath _mountPath;

public IFileSystem MountFileSystem { get; }

public Watcher(MountFileSystem fileSystem, string mountName, UPath path, IFileSystemWatcher watcher)
public Watcher(MountFileSystem fileSystem, UPath mountPath, UPath path, IFileSystemWatcher watcher)
: base(fileSystem, path, watcher)
{
_mountName = mountName;
_mountPath = mountPath;

MountFileSystem = watcher.FileSystem;
}

protected override UPath? TryConvertPath(UPath pathFromEvent)
{
if (_mountName != null)
if (!_mountPath.IsNull)
{
return UPath.Root / _mountName / pathFromEvent.ToRelative();
return _mountPath / pathFromEvent.ToRelative();
}
else
{
Expand All @@ -692,52 +691,71 @@ protected override UPath ConvertPathFromDelegate(UPath path)

private IFileSystem TryGetMountOrNext(ref UPath path)
{
string mountName;
return TryGetMountOrNext(ref path, out mountName);
return TryGetMountOrNext(ref path, out var _);
}

private IFileSystem TryGetMountOrNext(ref UPath path, out string mountName)
private IFileSystem TryGetMountOrNext(ref UPath path, out UPath mountPath)
{
mountName = null;
mountPath = null;
if (path.IsNull)
{
return null;
}

UPath mountSubPath;

mountName = path.GetFirstDirectory(out mountSubPath);
IFileSystem mountfs;
IFileSystem mountfs = null;
lock (_mounts)
{
_mounts.TryGetValue(mountName, out mountfs);
foreach (var kvp in _mounts)
{
if (path.IsInDirectory(kvp.Key, true))
{
mountPath = kvp.Key;
mountfs = kvp.Value;
path = new UPath(path.FullName.Substring(mountPath.FullName.Length)).ToAbsolute();
break;
}
}
}

if (mountfs != null)
{
path = mountSubPath.ToAbsolute();
return mountfs;
}
else if (NextFileSystem != null)
{
mountName = null;
return NextFileSystem;
}
mountName = null;
return null;

mountPath = null;
return NextFileSystem;
}

private static UPath NormalizeMountName(UPath name)
{
if (name.FullName[name.FullName.Length - 1] == UPath.DirectorySeparator)
return name;

return name.FullName + "/";
}

private void AssertMountName(UPath name)
{
name.AssertAbsolute();
name.AssertAbsolute(nameof(name));
if (name == UPath.Root)
{
throw new ArgumentException("The mount name cannot be a `/` root filesystem", nameof(name));
}
}

if (name.GetDirectory() != UPath.Root)
private class UPathLengthComparer : IComparer<UPath>
{
public int Compare(UPath x, UPath y)
{
throw new ArgumentException("The mount name cannot contain subpath and must contain only a root path e.g `/mount`", nameof(name));
// longest UPath first
var lengthCompare = y.FullName.Length.CompareTo(x.FullName.Length);
if (lengthCompare != 0)
{
return lengthCompare;
}

// then compare name if equal length (otherwise we get exceptions about duplicates)
return string.CompareOrdinal(x.FullName, y.FullName);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Zio/FilterPattern.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Zio
{
/// <summary>
/// Filter pattern compiler used for <see cref="Zio.Watcher.IFileSystemWatcher"/> implementation.
/// Filter pattern compiler used for <see cref="Zio.IFileSystemWatcher"/> implementation.
/// Use the method <see cref="Parse"/> to create a pattern.
/// </summary>
public struct FilterPattern
Expand Down
13 changes: 10 additions & 3 deletions src/Zio/UPathExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,21 +205,28 @@ public static bool IsInDirectory(this UPath path, UPath directory, bool recursiv
return false;
}

var dirHasTrailingSeparator = dir[dir.Length - 1] != UPath.DirectorySeparator;
if (target.Length == dir.Length)
{
// exact match due to the StartsWith above
// the directory parameter is interpreted as a directory so trailing separator isn't important
return true;
}

var dirHasTrailingSeparator = dir[dir.Length - 1] == UPath.DirectorySeparator;

if (!recursive)
{
// need to check if the directory part terminates
var lastSeparatorInTarget = target.LastIndexOf(UPath.DirectorySeparator);
var expectedLastSeparator = dir.Length - (dirHasTrailingSeparator ? 0 : 1);
var expectedLastSeparator = dir.Length - (dirHasTrailingSeparator ? 1 : 0);

if (lastSeparatorInTarget != expectedLastSeparator)
{
return false;
}
}

if (dirHasTrailingSeparator)
if (!dirHasTrailingSeparator)
{
// directory is missing ending slash, check that target has it
return target.Length > dir.Length && target[dir.Length] == UPath.DirectorySeparator;
Expand Down