Implement CompositeMountedFileProvider

This commit is contained in:
Steve Sanderson 2017-12-14 14:31:55 +00:00
parent aa63da5151
commit a81ad1830f
3 changed files with 264 additions and 2 deletions

View File

@ -0,0 +1,87 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.Extensions.FileProviders;
using System.Collections.Generic;
using System.Linq;
using System;
using System.IO;
namespace Microsoft.Blazor.Internal.Common.FileProviders
{
// This is like a CompositeFileProvider, except that:
// (a) the child providers can be mounted at non-root locations
// (b) the set of contents is immutable and fully indexed at construction time
// so that all subsequent reads are "O(dictionary lookup)" time
public class CompositeMountedFileProvider : InMemoryFileProvider
{
public CompositeMountedFileProvider(IEnumerable<(string, IFileProvider)> providers)
: base(GetCompositeContents(providers))
{
}
private static IEnumerable<IFileInfo> GetCompositeContents(
IEnumerable<(string, IFileProvider)> providers)
{
return providers
.Select(pair => new { MountPoint = pair.Item1, Files = ReadAllFiles(pair.Item2, string.Empty) })
.SelectMany(info => info.Files.Select(file => (IFileInfo)new MountedFileInfo(info.MountPoint, file)));
}
private static IEnumerable<IFileInfo> ReadAllFiles(IFileProvider provider, string subpath)
{
return provider.GetDirectoryContents(subpath).SelectMany(
item => item.IsDirectory
? ReadAllFiles(provider, item.PhysicalPath)
: new[] { item });
}
private class MountedFileInfo : IFileInfo
{
private readonly IFileInfo _file;
public MountedFileInfo(string mountPoint, IFileInfo file)
{
_file = file;
if (!file.PhysicalPath.StartsWith('/'))
{
throw new ArgumentException($"For mounted files, {nameof(file.PhysicalPath)} must start with '/'. Value supplied: '{file.PhysicalPath}'.");
}
if (!mountPoint.StartsWith('/'))
{
throw new ArgumentException("The path must start with '/'", nameof(mountPoint));
}
if (mountPoint == "/")
{
PhysicalPath = file.PhysicalPath;
}
else
{
if (mountPoint.EndsWith('/'))
{
throw new ArgumentException("Non-root paths must not end with '/'", nameof(mountPoint));
}
PhysicalPath = mountPoint + file.PhysicalPath;
}
}
public bool Exists => _file.Exists;
public long Length => _file.Length;
public string PhysicalPath { get; }
public string Name => _file.Name;
public DateTimeOffset LastModified => _file.LastModified;
public bool IsDirectory => _file.IsDirectory;
public Stream CreateReadStream() => _file.CreateReadStream();
}
}
}

View File

@ -20,10 +20,15 @@ namespace Microsoft.Blazor.Internal.Common.FileProviders
// It's convenient to use forward slash, because it matches URL conventions
public const char DirectorySeparatorChar = '/';
public InMemoryFileProvider(IEnumerable<(string, Stream)> contents)
public InMemoryFileProvider(IEnumerable<(string, Stream)> contents) : this(
contents.Select(pair => InMemoryFileInfo
.ForExistingFile(pair.Item1, pair.Item2, DateTime.Now)))
{
}
public InMemoryFileProvider(IEnumerable<IFileInfo> contents)
{
_filesByFullPath = contents
.Select(path => InMemoryFileInfo.ForExistingFile(path.Item1, path.Item2, DateTime.Now))
.ToDictionary(
fileInfo => fileInfo.PhysicalPath,
fileInfo => (IFileInfo)fileInfo);

View File

@ -0,0 +1,170 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.Blazor.Internal.Common.FileProviders;
using Microsoft.Extensions.FileProviders;
using System;
using System.IO;
using System.Linq;
using System.Text;
using Xunit;
namespace Microsoft.Blazor.Common.Test
{
public class CompositeMountedFileProviderTest
{
private (string, Stream) TestItem(string name) => TestItem(name, Array.Empty<byte>());
private (string, Stream) TestItem(string name, string data) => TestItem(name, Encoding.UTF8.GetBytes(data));
private (string, Stream) TestItem(string name, byte[] data) => (name, new MemoryStream(data));
private IFileProvider TestFileProvider(params string[] paths) => new InMemoryFileProvider(paths.Select(TestItem));
[Fact]
public void MountPointsMustStartWithSlash()
{
Assert.Throws<ArgumentException>(() =>
{
new CompositeMountedFileProvider(new[]
{
("test", TestFileProvider("/something.txt"))
});
});
}
[Fact]
public void NonRootMountPointsMustNotEndWithSlash()
{
Assert.Throws<ArgumentException>(() =>
{
new CompositeMountedFileProvider(new[]
{
("/test/", TestFileProvider("/something.txt"))
});
});
}
[Fact]
public void MountedFilePathsMustStartWithSlash()
{
Assert.Throws<ArgumentException>(() =>
{
new CompositeMountedFileProvider(new[]
{
("/test", TestFileProvider("something.txt"))
});
});
}
[Fact]
public void CanMountFileProviderAtRoot()
{
// Arrange
IFileProvider childProvider = new InMemoryFileProvider(new[]
{
TestItem("/rootitem.txt", "Root item contents"),
TestItem("/subdir/another", "Another test item"),
});
var instance = new CompositeMountedFileProvider(new[]
{
("/", childProvider)
});
// Act
var rootContents = instance.GetDirectoryContents(string.Empty);
var subdirContents = instance.GetDirectoryContents("/subdir");
// Assert
Assert.Collection(rootContents,
item =>
{
Assert.Equal("/rootitem.txt", item.PhysicalPath);
Assert.False(item.IsDirectory);
Assert.Equal("Root item contents", new StreamReader(item.CreateReadStream()).ReadToEnd());
},
item =>
{
Assert.Equal("/subdir", item.PhysicalPath);
Assert.True(item.IsDirectory);
});
Assert.Collection(subdirContents,
item =>
{
Assert.Equal("/subdir/another", item.PhysicalPath);
Assert.False(item.IsDirectory);
Assert.Equal("Another test item", new StreamReader(item.CreateReadStream()).ReadToEnd());
});
}
[Fact]
public void CanMountFileProvidersAtSubPaths()
{
// Arrange
var instance = new CompositeMountedFileProvider(new[]
{
("/dir", TestFileProvider("/first", "/A/second", "/A/third")),
("/dir/sub", TestFileProvider("/X", "/B/Y", "/B/Z")),
("/other", TestFileProvider("/final")),
});
// Act
var rootContents = instance.GetDirectoryContents("/");
var rootDirContents = instance.GetDirectoryContents("/dir");
var rootDirAContents = instance.GetDirectoryContents("/dir/A");
var rootDirSubContents = instance.GetDirectoryContents("/dir/sub");
var rootDirSubBContents = instance.GetDirectoryContents("/dir/sub/B");
var otherContents = instance.GetDirectoryContents("/other");
// Assert
Assert.Collection(rootContents,
item => Assert.Equal("/dir", item.PhysicalPath),
item => Assert.Equal("/other", item.PhysicalPath));
Assert.Collection(rootDirContents,
item => Assert.Equal("/dir/first", item.PhysicalPath),
item => Assert.Equal("/dir/A", item.PhysicalPath),
item => Assert.Equal("/dir/sub", item.PhysicalPath));
Assert.Collection(rootDirAContents,
item => Assert.Equal("/dir/A/second", item.PhysicalPath),
item => Assert.Equal("/dir/A/third", item.PhysicalPath));
Assert.Collection(rootDirSubContents,
item => Assert.Equal("/dir/sub/X", item.PhysicalPath),
item => Assert.Equal("/dir/sub/B", item.PhysicalPath));
Assert.Collection(rootDirSubBContents,
item => Assert.Equal("/dir/sub/B/Y", item.PhysicalPath),
item => Assert.Equal("/dir/sub/B/Z", item.PhysicalPath));
Assert.Collection(otherContents,
item => Assert.Equal("/other/final", item.PhysicalPath));
}
[Fact]
public void CanMountMultipleFileProvidersAtSameLocation()
{
// Arrange
var instance = new CompositeMountedFileProvider(new[]
{
("/dir", TestFileProvider("/first")),
("/dir", TestFileProvider("/second"))
});
// Act
var contents = instance.GetDirectoryContents("/dir");
// Assert
Assert.Collection(contents,
item => Assert.Equal("/dir/first", item.PhysicalPath),
item => Assert.Equal("/dir/second", item.PhysicalPath));
}
[Fact]
public void DisallowsOverlappingFiles()
{
Assert.Throws<ArgumentException>(() =>
{
new CompositeMountedFileProvider(new[]
{
("/dir", TestFileProvider("/file")),
("/", TestFileProvider("/dir/file"))
});
});
}
}
}