Add fallback routing to file
This is a new routing feature that integrates static files to serve a static file when routing doesn't match anything else. This is a scenario that's covered by SPA services today, but given the improvements to routing it makes much more sense to move lower in the stack.
This commit is contained in:
parent
7e63e2da43
commit
089f64c528
|
|
@ -7,6 +7,7 @@
|
||||||
<Compile Include="Microsoft.AspNetCore.StaticFiles.netcoreapp3.0.cs" />
|
<Compile Include="Microsoft.AspNetCore.StaticFiles.netcoreapp3.0.cs" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
|
<Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
|
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Routing" />
|
||||||
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
|
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
|
||||||
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
|
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||||
<Reference Include="Microsoft.Extensions.WebEncoders" />
|
<Reference Include="Microsoft.Extensions.WebEncoders" />
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,13 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
public System.Action<Microsoft.AspNetCore.StaticFiles.StaticFileResponseContext> OnPrepareResponse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public System.Action<Microsoft.AspNetCore.StaticFiles.StaticFileResponseContext> OnPrepareResponse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
public bool ServeUnknownFileTypes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public bool ServeUnknownFileTypes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
}
|
}
|
||||||
|
public static partial class StaticFilesEndpointRouteBuilderExtensions
|
||||||
|
{
|
||||||
|
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToFile(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder builder, string filePath) { throw null; }
|
||||||
|
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToFile(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder builder, string filePath, Microsoft.AspNetCore.Builder.StaticFileOptions options) { throw null; }
|
||||||
|
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToFile(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder builder, string pattern, string filePath) { throw null; }
|
||||||
|
public static Microsoft.AspNetCore.Builder.IEndpointConventionBuilder MapFallbackToFile(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder builder, string pattern, string filePath, Microsoft.AspNetCore.Builder.StaticFileOptions options) { throw null; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
namespace Microsoft.AspNetCore.StaticFiles
|
namespace Microsoft.AspNetCore.StaticFiles
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Description>ASP.NET Core static files middleware. Includes middleware for serving static files, directory browsing, and default files.</Description>
|
<Description>ASP.NET Core static files middleware. Includes middleware for serving static files, directory browsing, and default files.</Description>
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
|
<Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
|
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Routing" />
|
||||||
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
|
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
|
||||||
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
|
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||||
<Reference Include="Microsoft.Extensions.WebEncoders" />
|
<Reference Include="Microsoft.Extensions.WebEncoders" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
// 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 Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Endpoints;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.AspNetCore.StaticFiles;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Builder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains extension methods for using static files with endpoint routing.
|
||||||
|
/// </summary>
|
||||||
|
public static class StaticFilesEndpointRouteBuilderExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||||
|
/// requests for non-filenames with the lowest possible priority. The request will be routed to a
|
||||||
|
/// <see cref="StaticFileMiddleware"/> that attempts to serve the file specified by <paramref name="filePath"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/>.</param>
|
||||||
|
/// <param name="filePath">The file path of the file to serve.</param>
|
||||||
|
/// <returns>The <see cref="IEndpointRouteBuilder"/></returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="MapFallbackToFile(IEndpointRouteBuilder, string)"/> is intended to handle cases where URL path of
|
||||||
|
/// the request does not contain a filename, and no other endpoint has matched. This is convenient for routing
|
||||||
|
/// requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to
|
||||||
|
/// result in an HTTP 404.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// The default <see cref="StaticFileOptions"/> for the <see cref="StaticFileMiddleware"/> will be used.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="MapFallbackToFile(IEndpointRouteBuilder, string)"/> registers an endpoint using the pattern
|
||||||
|
/// <c>{*path:nonfile}</c>. The order of the registered endpoint will be <c>int.MaxValue</c>.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public static IEndpointConventionBuilder MapFallbackToFile(
|
||||||
|
this IEndpointRouteBuilder builder,
|
||||||
|
string filePath)
|
||||||
|
{
|
||||||
|
if (builder == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(builder));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filePath == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.MapFallback(CreateRequestDelegate(builder, filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||||
|
/// requests for non-filenames with the lowest possible priority. The request will be routed to a
|
||||||
|
/// <see cref="StaticFileMiddleware"/> that attempts to serve the file specified by <paramref name="filePath"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/>.</param>
|
||||||
|
/// <param name="filePath">The file path of the file to serve.</param>
|
||||||
|
/// <param name="options"><see cref="StaticFileOptions"/> for the <see cref="StaticFileMiddleware"/>.</param>
|
||||||
|
/// <returns>The <see cref="IEndpointRouteBuilder"/></returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="MapFallbackToFile(IEndpointRouteBuilder, string, StaticFileOptions)"/> is intended to handle cases
|
||||||
|
/// where URL path of the request does not contain a file name, and no other endpoint has matched. This is convenient
|
||||||
|
/// for routing requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to
|
||||||
|
/// result in an HTTP 404.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="MapFallbackToFile(IEndpointRouteBuilder, string, StaticFileOptions)"/> registers an endpoint using the pattern
|
||||||
|
/// <c>{*path:nonfile}</c>. The order of the registered endpoint will be <c>int.MaxValue</c>.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public static IEndpointConventionBuilder MapFallbackToFile(
|
||||||
|
this IEndpointRouteBuilder builder,
|
||||||
|
string filePath,
|
||||||
|
StaticFileOptions options)
|
||||||
|
{
|
||||||
|
if (builder == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(builder));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filePath == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.MapFallback(CreateRequestDelegate(builder, filePath, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||||
|
/// requests for non-filenames with the lowest possible priority. The request will be routed to a
|
||||||
|
/// <see cref="StaticFileMiddleware"/> that attempts to serve the file specified by <paramref name="filePath"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/>.</param>
|
||||||
|
/// <param name="pattern">The route pattern.</param>
|
||||||
|
/// <param name="filePath">The file path of the file to serve.</param>
|
||||||
|
/// <returns>The <see cref="IEndpointRouteBuilder"/></returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="MapFallbackToFile(IEndpointRouteBuilder, string, string)"/> is intended to handle
|
||||||
|
/// cases where URL path of the request does not contain a filename, and no other endpoint has matched. This is
|
||||||
|
/// convenient for routing requests for dynamic content to a SPA framework, while also allowing requests for
|
||||||
|
/// non-existent files to result in an HTTP 404.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// The default <see cref="StaticFileOptions"/> for the <see cref="StaticFileMiddleware"/> will be used.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// The order of the registered endpoint will be <c>int.MaxValue</c>.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// This overload will use the provided <paramref name="pattern"/> verbatim. Use the <c>:nonfile</c> route contraint
|
||||||
|
/// to exclude requests for static files.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public static IEndpointConventionBuilder MapFallbackToFile(
|
||||||
|
this IEndpointRouteBuilder builder,
|
||||||
|
string pattern,
|
||||||
|
string filePath)
|
||||||
|
{
|
||||||
|
if (builder == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(builder));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pattern == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filePath == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.MapFallback(pattern, CreateRequestDelegate(builder, filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
|
||||||
|
/// requests for non-filenames with the lowest possible priority. The request will be routed to a
|
||||||
|
/// <see cref="StaticFileMiddleware"/> that attempts to serve the file specified by <paramref name="filePath"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/>.</param>\
|
||||||
|
/// <param name="pattern">The route pattern.</param>
|
||||||
|
/// <param name="filePath">The file path of the file to serve.</param>
|
||||||
|
/// <param name="options"><see cref="StaticFileOptions"/> for the <see cref="StaticFileMiddleware"/>.</param>
|
||||||
|
/// <returns>The <see cref="IEndpointRouteBuilder"/></returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="MapFallbackToFile(IEndpointRouteBuilder, string, string, StaticFileOptions)"/> is intended to handle
|
||||||
|
/// cases where URL path of the request does not contain a filename, and no other endpoint has matched. This is
|
||||||
|
/// convenient for routing requests for dynamic content to a SPA framework, while also allowing requests for
|
||||||
|
/// non-existent files to result in an HTTP 404.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// The order of the registered endpoint will be <c>int.MaxValue</c>.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// This overload will use the provided <paramref name="pattern"/> verbatim. Use the <c>:nonfile</c> route contraint
|
||||||
|
/// to exclude requests for static files.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public static IEndpointConventionBuilder MapFallbackToFile(
|
||||||
|
this IEndpointRouteBuilder builder,
|
||||||
|
string pattern,
|
||||||
|
string filePath,
|
||||||
|
StaticFileOptions options)
|
||||||
|
{
|
||||||
|
if (builder == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(builder));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pattern == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filePath == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.MapFallback(pattern, CreateRequestDelegate(builder, filePath, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RequestDelegate CreateRequestDelegate(
|
||||||
|
IEndpointRouteBuilder builder,
|
||||||
|
string filePath,
|
||||||
|
StaticFileOptions options = null)
|
||||||
|
{
|
||||||
|
var app = builder.CreateApplicationBuilder();
|
||||||
|
app.Use(next => context =>
|
||||||
|
{
|
||||||
|
context.Request.Path = "/" + filePath;
|
||||||
|
|
||||||
|
// Set endpoint to null so the static files middleware will handle the request.
|
||||||
|
context.SetEndpoint(null);
|
||||||
|
|
||||||
|
return next(context);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
app.UseStaticFiles();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
app.UseStaticFiles(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
// 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.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Server.IntegrationTesting;
|
||||||
|
using Microsoft.AspNetCore.Server.IntegrationTesting.Common;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.StaticFiles
|
||||||
|
{
|
||||||
|
public class FallbackStaticFileTest : LoggedTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ReturnsFileForDefaultPattern()
|
||||||
|
{
|
||||||
|
var builder = new WebHostBuilder()
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.AddRouting();
|
||||||
|
services.AddSingleton(LoggerFactory);
|
||||||
|
})
|
||||||
|
.UseKestrel()
|
||||||
|
.UseWebRoot(AppContext.BaseDirectory)
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
var environment = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
|
||||||
|
|
||||||
|
app.UseRouting(routes =>
|
||||||
|
{
|
||||||
|
routes.Map("/hello", context =>
|
||||||
|
{
|
||||||
|
return context.Response.WriteAsync("Hello, world!");
|
||||||
|
});
|
||||||
|
|
||||||
|
routes.MapFallbackToFile("default.html", new StaticFileOptions()
|
||||||
|
{
|
||||||
|
FileProvider = new PhysicalFileProvider(Path.Combine(environment.WebRootPath, "SubFolder")),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var server = builder.Start(TestUrlHelper.GetTestUrl(ServerType.Kestrel)))
|
||||||
|
{
|
||||||
|
var environment = server.Services.GetRequiredService<IWebHostEnvironment>();
|
||||||
|
using (var client = new HttpClient { BaseAddress = new Uri(server.GetAddress()) })
|
||||||
|
{
|
||||||
|
var response = await client.GetAsync("hello");
|
||||||
|
var responseText = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Equal("Hello, world!", responseText);
|
||||||
|
|
||||||
|
response = await client.GetAsync("/");
|
||||||
|
var responseContent = await response.Content.ReadAsByteArrayAsync();
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
AssertFileEquals(environment, "SubFolder/default.html", responseContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReturnsFileForCustomPattern()
|
||||||
|
{
|
||||||
|
var builder = new WebHostBuilder()
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.AddRouting();
|
||||||
|
services.AddSingleton(LoggerFactory);
|
||||||
|
})
|
||||||
|
.UseKestrel()
|
||||||
|
.UseWebRoot(AppContext.BaseDirectory)
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseRouting(routes =>
|
||||||
|
{
|
||||||
|
routes.Map("/hello", context =>
|
||||||
|
{
|
||||||
|
return context.Response.WriteAsync("Hello, world!");
|
||||||
|
});
|
||||||
|
|
||||||
|
routes.MapFallbackToFile("/prefix/{*path:nonfile}", "TestDocument.txt");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var server = builder.Start(TestUrlHelper.GetTestUrl(ServerType.Kestrel)))
|
||||||
|
{
|
||||||
|
var environment = server.Services.GetRequiredService<IWebHostEnvironment>();
|
||||||
|
using (var client = new HttpClient { BaseAddress = new Uri(server.GetAddress()) })
|
||||||
|
{
|
||||||
|
var response = await client.GetAsync("hello");
|
||||||
|
var responseText = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Equal("Hello, world!", responseText);
|
||||||
|
|
||||||
|
response = await client.GetAsync("prefix/Some-Path");
|
||||||
|
var responseContent = await response.Content.ReadAsByteArrayAsync();
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
AssertFileEquals(environment, "TestDocument.txt", responseContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertFileEquals(IWebHostEnvironment environment, string filePath, byte[] responseContent)
|
||||||
|
{
|
||||||
|
var fileInfo = environment.WebRootFileProvider.GetFileInfo(filePath);
|
||||||
|
Assert.NotNull(fileInfo);
|
||||||
|
Assert.True(fileInfo.Exists);
|
||||||
|
|
||||||
|
using (var stream = fileInfo.CreateReadStream())
|
||||||
|
{
|
||||||
|
var fileContents = new byte[stream.Length];
|
||||||
|
stream.Read(fileContents, 0, (int)stream.Length);
|
||||||
|
Assert.True(responseContent.SequenceEqual(fileContents));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue