|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders;
namespace Microsoft.AspNetCore.StaticFiles;
/// <summary>
/// Generates an HTML view for a directory.
/// </summary>
public class HtmlDirectoryFormatter : IDirectoryFormatter
{
private const string TextHtmlUtf8 = "text/html; charset=utf-8";
private readonly HtmlEncoder _htmlEncoder;
/// <summary>
/// Constructs the <see cref="HtmlDirectoryFormatter"/>.
/// </summary>
/// <param name="encoder">The character encoding representation to use.</param>
public HtmlDirectoryFormatter(HtmlEncoder encoder)
{
ArgumentNullException.ThrowIfNull(encoder);
_htmlEncoder = encoder;
}
/// <summary>
/// Generates an HTML view for a directory.
/// </summary>
public virtual Task GenerateContentAsync(HttpContext context, IEnumerable<IFileInfo> contents)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(contents);
context.Response.ContentType = TextHtmlUtf8;
if (HttpMethods.IsHead(context.Request.Method))
{
// HEAD, no response body
return Task.CompletedTask;
}
PathString requestPath = context.Request.PathBase + context.Request.Path;
var builder = new StringBuilder();
builder.AppendFormat(
CultureInfo.InvariantCulture,
@"<!DOCTYPE html>
<html lang=""{0}"">", CultureInfo.CurrentUICulture.TwoLetterISOLanguageName);
builder.AppendFormat(
CultureInfo.InvariantCulture,
@"
<head>
<title>{0} {1}</title>", HtmlEncode(Resources.HtmlDir_IndexOf), HtmlEncode(requestPath.Value!));
builder.Append(@"
<style>
body {
font-family: ""Segoe UI"", ""Segoe WP"", ""Helvetica Neue"", 'RobotoRegular', sans-serif;
font-size: 14px;}
header h1 {
font-family: ""Segoe UI Light"", ""Helvetica Neue"", 'RobotoLight', ""Segoe UI"", ""Segoe WP"", sans-serif;
font-size: 28px;
font-weight: 100;
margin-top: 5px;
margin-bottom: 0px;}
#index {
border-collapse: separate;
border-spacing: 0;
margin: 0 0 20px; }
#index th {
vertical-align: bottom;
padding: 10px 5px 5px 5px;
font-weight: 400;
color: #a0a0a0;
text-align: center; }
#index td { padding: 3px 10px; }
#index th, #index td {
border-right: 1px #ddd solid;
border-bottom: 1px #ddd solid;
border-left: 1px transparent solid;
border-top: 1px transparent solid;
box-sizing: border-box; }
#index th:last-child, #index td:last-child {
border-right: 1px transparent solid; }
#index td.length, td.modified { text-align:right; }
a { color:#1ba1e2;text-decoration:none; }
a:hover { color:#13709e;text-decoration:underline; }
</style>
</head>
<body>
<section id=""main"">");
builder.AppendFormat(
CultureInfo.InvariantCulture,
@"
<header><h1>{0} <a href=""/"">/</a>", HtmlEncode(Resources.HtmlDir_IndexOf));
string cumulativePath = "/";
foreach (var segment in requestPath.Value!.Split('/', StringSplitOptions.RemoveEmptyEntries))
{
cumulativePath = cumulativePath + segment + "/";
builder.AppendFormat(
CultureInfo.InvariantCulture,
@"<a href=""{0}"">{1}/</a>",
HtmlEncode(cumulativePath), HtmlEncode(segment));
}
builder.AppendFormat(
CultureInfo.InvariantCulture,
@"</h1></header>
<table id=""index"" summary=""{0}"">
<thead>
<tr><th abbr=""{1}"">{1}</th><th abbr=""{2}"">{2}</th><th abbr=""{3}"">{4}</th></tr>
</thead>
<tbody>",
HtmlEncode(Resources.HtmlDir_TableSummary),
HtmlEncode(Resources.HtmlDir_Name),
HtmlEncode(Resources.HtmlDir_Size),
HtmlEncode(Resources.HtmlDir_Modified),
HtmlEncode(Resources.HtmlDir_LastModified));
foreach (var subdir in contents.Where(info => info.IsDirectory))
{
// Collect directory metadata in a try...catch in case the file is deleted while we're getting the data.
// The metadata is retrieved prior to calling AppendFormat so if it throws, we won't have written a row
// to the table.
try
{
builder.AppendFormat(
CultureInfo.InvariantCulture,
@"
<tr class=""directory"">
<td class=""name""><a href=""./{0}/"">{0}/</a></td>
<td></td>
<td class=""modified"">{1}</td>
</tr>",
HtmlEncode(subdir.Name),
HtmlEncode(subdir.LastModified.ToString(CultureInfo.CurrentCulture)));
}
catch (DirectoryNotFoundException)
{
// The physical DirectoryInfo class doesn't appear to throw for either
// of Name or LastWriteTimeUtc (which backs LastModified in the physical provider)
// if the directory doesn't exist. However, we don't know what other providers might do.
// Just skip this directory. It was deleted while we were enumerating.
}
catch (FileNotFoundException)
{
// The physical DirectoryInfo class doesn't appear to throw for either
// of Name or LastWriteTimeUtc (which backs LastModified in the physical provider)
// if the directory doesn't exist. However, we don't know what other providers might do.
// Just skip this directory. It was deleted while we were enumerating.
}
}
foreach (var file in contents.Where(info => !info.IsDirectory))
{
// Collect file metadata in a try...catch in case the file is deleted while we're getting the data.
// The metadata is retrieved prior to calling AppendFormat so if it throws, we won't have written a row
// to the table.
try
{
builder.AppendFormat(
CultureInfo.InvariantCulture,
@"
<tr class=""file"">
<td class=""name""><a href=""./{0}"">{0}</a></td>
<td class=""length"">{1}</td>
<td class=""modified"">{2}</td>
</tr>",
HtmlEncode(file.Name),
HtmlEncode(file.Length.ToString("n0", CultureInfo.CurrentCulture)),
HtmlEncode(file.LastModified.ToString(CultureInfo.CurrentCulture)));
}
catch (DirectoryNotFoundException)
{
// There doesn't appear to be a case where DirectoryNotFound is thrown in the physical provider,
// but we don't know what other providers might do.
// Just skip this file. It was deleted while we were enumerating.
}
catch (FileNotFoundException)
{
// Just skip this file. It was deleted while we were enumerating.
}
}
builder.Append(@"
</tbody>
</table>
</section>
</body>
</html>");
string data = builder.ToString();
byte[] bytes = Encoding.UTF8.GetBytes(data);
context.Response.ContentLength = bytes.Length;
return context.Response.Body.WriteAsync(bytes, 0, bytes.Length);
}
private string HtmlEncode(string body)
{
return _htmlEncoder.Encode(body);
}
}
|