Skip to content
This repository was archived by the owner on Nov 6, 2018. It is now read-only.

Implement a CombinedFileProvider #142

Closed
wants to merge 10 commits into from
33 changes: 31 additions & 2 deletions FileSystem.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.22823.1
VisualStudioVersion = 14.0.23107.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A1477614-E825-4204-A684-385004B63AEB}"
EndProject
Expand All @@ -28,6 +27,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.FileProvid
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.FileProviders.Sources", "src\Microsoft.AspNet.FileProviders.Sources\Microsoft.AspNet.FileProviders.Sources.xproj", "{92C2C85C-D1A5-44BD-BE23-238E08471B4D}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.FileProviders.Composite", "src\Microsoft.AspNet.FileProviders.Composite\Microsoft.AspNet.FileProviders.Composite.xproj", "{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.FileProviders.Composite.Tests", "test\Microsoft.AspNet.FileProviders.Composite.Tests\Microsoft.AspNet.FileProviders.Composite.Tests.xproj", "{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -132,6 +135,30 @@ Global
{92C2C85C-D1A5-44BD-BE23-238E08471B4D}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{92C2C85C-D1A5-44BD-BE23-238E08471B4D}.Release|x86.ActiveCfg = Release|Any CPU
{92C2C85C-D1A5-44BD-BE23-238E08471B4D}.Release|x86.Build.0 = Release|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|x86.ActiveCfg = Debug|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Debug|x86.Build.0 = Debug|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|Any CPU.Build.0 = Release|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|x86.ActiveCfg = Release|Any CPU
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254}.Release|x86.Build.0 = Release|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|x86.ActiveCfg = Debug|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Debug|x86.Build.0 = Debug|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|Any CPU.Build.0 = Release|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|x86.ActiveCfg = Release|Any CPU
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -145,5 +172,7 @@ Global
{6B6BA57A-B32D-430A-AF39-09CAA85308C2} = {E399495E-82B8-4C06-8779-C1D02BEF4495}
{66FE5FDF-BBF9-4573-A7B7-53551731C0F9} = {E399495E-82B8-4C06-8779-C1D02BEF4495}
{92C2C85C-D1A5-44BD-BE23-238E08471B4D} = {A1477614-E825-4204-A684-385004B63AEB}
{CAAF52EF-F91B-474D-AC56-FE9D96FF8254} = {A1477614-E825-4204-A684-385004B63AEB}
{C2EA9BD0-C986-4B60-9E45-5EA51E1EA494} = {E399495E-82B8-4C06-8779-C1D02BEF4495}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// 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 System;
using System.Collections;
using System.Collections.Generic;

namespace Microsoft.AspNet.FileProviders
{
/// <summary>
/// Represents the result of a call composition of <see cref="IFileProvider.GetDirectoryContents(string)"/> for a list of <see cref="IFileProvider"/> and a path.
/// </summary>
public class CompositeDirectoryContents : IDirectoryContents
{
private readonly IList<IFileProvider> _fileProviders;
private readonly string _subPath;
private List<IFileInfo> _files;
private bool _exists;
private List<IDirectoryContents> _directories;

/// <summary>
/// Creates a new instance of <see cref="CompositeDirectoryContents"/> to represents the result of a call composition of <see cref="IFileProvider.GetDirectoryContents(string)"/>.
/// </summary>
/// <param name="fileProviders">The list of <see cref="IFileProvider"/> for which the results have to be composed.</param>
/// <param name="subpath">The path.</param>
public CompositeDirectoryContents(IList<IFileProvider> fileProviders, string subpath)
{
if(fileProviders == null)
{
throw new ArgumentNullException(nameof(fileProviders));
}
_fileProviders = fileProviders;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would need a null check (throw new ArgumentNullException)

_subPath = subpath;
}

private void EnsureDirectoriesAreInitialized()
{
if (_directories == null)
{
_directories = new List<IDirectoryContents>();
foreach (var fileProvider in _fileProviders)
{
var directoryContents = fileProvider.GetDirectoryContents(_subPath);
if (directoryContents != null && directoryContents.Exists)
{
_exists = true;
_directories.Add(directoryContents);
}
}
}
}

private void EnsureFilesAreInitialized()
{
EnsureDirectoriesAreInitialized();
if (_files == null)
{
_files = new List<IFileInfo>();
var names = new HashSet<string>();
for (var i = 0; i < _directories.Count; i++)
{
var directoryContents = _directories[i];
foreach (var file in directoryContents)
{
if (names.Add(file.Name))
{
_files.Add(file);
}
}
}
}
}

public IEnumerator<IFileInfo> GetEnumerator()
{
EnsureFilesAreInitialized();
return _files.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
EnsureFilesAreInitialized();
return _files.GetEnumerator();
}

public bool Exists
{
get
{
EnsureDirectoriesAreInitialized();
return _exists;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// 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 System;
using System.Collections.Generic;

namespace Microsoft.AspNet.FileProviders
{
/// <summary>
/// Represents a composition of <see cref="IDisposable"/>.
/// </summary>
public class CompositeDisposable : IDisposable
{
private readonly IList<IDisposable> _disposables;
/// <summary>
/// Creates a new instance of <see cref="CompositeDisposable"/>.
/// </summary>
/// <param name="disposables">The list of <see cref="IDisposable"/> to compose.</param>
public CompositeDisposable(IList<IDisposable> disposables)
{
if(disposables == null)
{
throw new ArgumentNullException(nameof(disposables));
}
_disposables = disposables;
}

public void Dispose()
{
for (var i = 0; i < _disposables.Count; i++)
{
_disposables[i].Dispose();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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 System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNet.FileProviders
{
/// <summary>
/// Represents a composition of <see cref="IChangeToken"/>.
/// </summary>
public class CompositeFileChangeToken : IChangeToken
{
private readonly IList<IChangeToken> _changeTokens;

/// <summary>
/// Creates a new instance of <see cref="CompositeFileChangeToken"/>.
/// </summary>
/// <param name="changeTokens">The list of <see cref="IChangeToken"/> to compose.</param>
public CompositeFileChangeToken(IList<IChangeToken> changeTokens)
{
if(changeTokens == null)
{
throw new ArgumentNullException(nameof(changeTokens));
}
_changeTokens = changeTokens;
}

public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
var disposables = new List<IDisposable>();
for (var i = 0; i < _changeTokens.Count; i++)
{
var changeToken = _changeTokens[i];
if (changeToken.ActiveChangeCallbacks)
{
var disposable = _changeTokens[i].RegisterChangeCallback(callback, state);
disposables.Add(disposable);
}
}
return new CompositeDisposable(disposables);
}

public bool HasChanged
{
get { return _changeTokens.Any(token => token.HasChanged); }
}

public bool ActiveChangeCallbacks
{
get { return _changeTokens.Any(token => token.ActiveChangeCallbacks); }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// 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 System.Collections.Generic;
using Microsoft.Extensions.Primitives;
using System;
using System.Linq;

namespace Microsoft.AspNet.FileProviders
{
/// <summary>
/// Looks up files using a list of <see cref="IFileProvider"/>.
/// </summary>
public class CompositeFileProvider : IFileProvider
{
private readonly IFileProvider[] _fileProviders;

/// <summary>
/// Initializes a new instance of the <see cref="CompositeFileProvider" /> class using a list of file provider.
/// </summary>
/// <param name="fileProviders"></param>
public CompositeFileProvider(params IFileProvider[] fileProviders)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we settle on using params? I thought @davidfowl wanted to use an IEnumerable here?

{
_fileProviders = fileProviders ?? new IFileProvider[0];
}
/// <summary>
/// Initializes a new instance of the <see cref="CompositeFileProvider" /> class using a list of file provider.
/// </summary>
/// <param name="fileProviders"></param>
public CompositeFileProvider(IEnumerable<IFileProvider> fileProviders)
{
if(fileProviders == null)
{
throw new ArgumentNullException(nameof(fileProviders));
}
_fileProviders = fileProviders.ToArray();
}

/// <summary>
/// Locates a file at the given path.
/// </summary>
/// <param name="subpath">The path that identifies the file. </param>
/// <returns>The file information. Caller must check Exists property. This will be the first existing <see cref="IFileInfo"/> returned by the provided <see cref="IFileProvider"/> or a not found <see cref="IFileInfo"/> if no existing files is found.</returns>
public IFileInfo GetFileInfo(string subpath)
{
foreach (var fileProvider in _fileProviders)
{
var fileInfo = fileProvider.GetFileInfo(subpath);
if (fileInfo != null && fileInfo.Exists)
{
return fileInfo;
}
}
return new NotFoundFileInfo(subpath);
}

/// <summary>
/// Enumerate a directory at the given path, if any.
/// </summary>
/// <param name="subpath">The path that identifies the directory</param>
/// <returns>Contents of the directory. Caller must check Exists property.
/// The content is a merge of the contents of the provided <see cref="IFileProvider"/>.
/// When there is multiple <see cref="IFileInfo"/> with the same Name property, only the first one is included on the results.</returns>
public IDirectoryContents GetDirectoryContents(string subpath)
{
var directoryContents = new CompositeDirectoryContents(_fileProviders, subpath);
return directoryContents;
}

/// <summary>
/// Creates a <see cref="IChangeToken"/> for the specified <paramref name="filter"/>.
/// </summary>
/// <remarks></remarks>
/// <param name="pattern">Filter string used to determine what files or folders to monitor. Example: **/*.cs, *.*, subFolder/**/*.cshtml.</param>
/// <returns>An <see cref="IChangeToken"/> that is notified when a file matching <paramref name="filter"/> is added, modified or deleted.
/// The change token will be notified when one of the change token returned by the provided <see cref="IFileProvider"/> will be notified.</returns>
public IChangeToken Watch(string pattern)
{
// Watch all file providers
var changeTokens = new List<IChangeToken>();
foreach (var fileProvider in _fileProviders)
{
var changeToken = fileProvider.Watch(pattern);
if (changeToken != null)
{
changeTokens.Add(changeToken);
}
}

// There is no change token with active change callbacks
if (changeTokens.Count == 0)
{
return NoopChangeToken.Singleton;
}
var CompositeFileChangeToken = new CompositeFileChangeToken(changeTokens);
return CompositeFileChangeToken;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="__ToolsVersion__" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>caaf52ef-f91b-474d-ac56-fe9d96ff8254</ProjectGuid>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// 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 System.Reflection;
using System.Resources;

[assembly: AssemblyMetadata("Serviceable", "True")]
[assembly: NeutralResourcesLanguage("en-us")]
Loading