Implement CompositeMountedFileProvider
This commit is contained in:
parent
aa63da5151
commit
a81ad1830f
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,10 +20,15 @@ namespace Microsoft.Blazor.Internal.Common.FileProviders
|
||||||
// It's convenient to use forward slash, because it matches URL conventions
|
// It's convenient to use forward slash, because it matches URL conventions
|
||||||
public const char DirectorySeparatorChar = '/';
|
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
|
_filesByFullPath = contents
|
||||||
.Select(path => InMemoryFileInfo.ForExistingFile(path.Item1, path.Item2, DateTime.Now))
|
|
||||||
.ToDictionary(
|
.ToDictionary(
|
||||||
fileInfo => fileInfo.PhysicalPath,
|
fileInfo => fileInfo.PhysicalPath,
|
||||||
fileInfo => (IFileInfo)fileInfo);
|
fileInfo => (IFileInfo)fileInfo);
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue