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
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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