-
Notifications
You must be signed in to change notification settings - Fork 0
/
PublicContentController.cs
353 lines (289 loc) · 13.2 KB
/
PublicContentController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
using DNDocs.Docs.Web.Model;
using DNDocs.Docs.Web.Service;
using DNDocs.Docs.Web.Shared;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Diagnostics.ResourceMonitoring;
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Net.Http.Headers;
using System.Text;
using Vinca.Http;
using Vinca.Http.Cache;
namespace DNDocs.Docs.Web.Web
{
public class PublicContentController
{
public static readonly ApiEndpoint[] Endpoints = new ApiEndpoint[]
{
GetEndpoint(HttpMethod.Get, "/n/{nugetPackageName}/{nugetPackageVersion}/{*slug}", GetNugetProjectSiteHtml),
GetEndpoint(HttpMethod.Get, "/v/{urlPrefix}/{versionTag}/{*slug}", GetVersionProjectSiteHtml),
GetEndpoint(HttpMethod.Get, "/s/{urlPrefix}/{*slug}", GetSingletonProjectSiteHtml),
GetEndpoint(HttpMethod.Get, "/public/{*slug}", GetPublicHtmlFile),
GetEndpoint(HttpMethod.Get, "/public/ping", Ping),
GetEndpoint(HttpMethod.Get, "/sitemap.xml", GetSiteMap),
GetEndpoint(HttpMethod.Get, "/robots.txt", GetRobotsTxt),
GetEndpoint(HttpMethod.Get, "/system/projects/{pageNo?}", SystemAllProjects),
GetEndpoint(HttpMethod.Get, "/system/site-items/{pageNo?}", SystemSiteItems),
GetEndpoint(HttpMethod.Get, "/system/stats", SystemStats),
GetEndpoint(HttpMethod.Get, "/system/resource-monitor", SystemResourceMonitoring),
};
static ApiEndpoint GetEndpoint(HttpMethod method, string path, Delegate delegateMethod)
{
return new ApiEndpoint(method, path, delegateMethod);
}
static IResult Ping()
{
return Results.Ok();
}
static async Task<IResult> GetRobotsTxt(
[FromServices] ILogger<PublicContentController> logger,
[FromServices] IWebHostEnvironment env) => await GetPublicHtmlFile(logger, env, "/public/robots.txt");
static async Task<IResult> GetPublicHtmlFile(
[FromServices] ILogger<PublicContentController> logger,
[FromServices] IWebHostEnvironment env,
string slug)
{
//todo add everyting from PublicHtml to site.sqlite db on startup
var publicHtmlDir = env.ContentRootPath;
var filepath = Path.Combine(publicHtmlDir, "PublicHtml", slug);
if (!File.Exists(filepath)) return Results.NotFound();
var file = new FileInfo(filepath);
if (string.Compare(file.FullName, filepath, StringComparison.OrdinalIgnoreCase) != 0)
{
logger.LogWarning("file from url not match file on disk. \r\nfile from url:\r\n'{}'\r\nfile from disk:\r\n'{1}'", filepath, file.FullName);
return Results.NotFound();
}
return Results.File(await File.ReadAllBytesAsync(filepath), HttpContenTypeMaps.GetFromPathOrFallback(filepath));
}
static void GetSiteMap()
{
}
static async Task<IResult> GetSingletonProjectSiteHtml(
[FromServices] IQRepository repository,
[FromRoute] string urlPrefix, string slug)
{
if (string.IsNullOrWhiteSpace(urlPrefix)) return Results.NotFound();
if (string.IsNullOrWhiteSpace(slug)) return Results.NotFound();
Project project = await repository.SelectSingletonProjectAsync(urlPrefix);
if (project == null) return Results.NotFound();
return await ReturnSiteItem(repository, project.Id, slug);
}
static async Task<IResult> GetVersionProjectSiteHtml([FromRoute] string urlPrefix, [FromRoute] string versionTag, string slug)
{
return Results.Content("not implemented", "text/plain");
}
[VCacheControl(CacheType = CacheControlType.Public, MaxAge = 30 * 60)]
static async Task<IResult> GetNugetProjectSiteHtml(
[FromServices] IOptions<DSettings> dsettings,
[FromServices] IQRepository repository,
[FromRoute] string nugetPackageName,
[FromRoute] string nugetPackageVersion,
string slug)
{
// todo when case invalid (e.g. AUTOmapper instead of AutoMapper) do redirect to valid case (must include {*slug});
// should be case sensitive
// todo big in memory cache of: projects, shared_site_item
// todo cache instead of db call
if (slug == null) return Results.NotFound();
Project project = await repository.SelectNugetProjectAsync(nugetPackageName, nugetPackageVersion);
if (project == null) return Results.Redirect(dsettings.Value.GetUrlNugetProjectGenerate(nugetPackageName, nugetPackageVersion));
return await ReturnSiteItem(repository, project.Id, slug);
}
private static async Task<IResult> ReturnSiteItem(IQRepository repository, long projectId, string slug)
{
var path = $"/{slug}";
var siteItem = await repository.SelectSiteItemAsync(projectId, path);
if (siteItem == null) return Results.NotFound();
byte[] byteData = siteItem.ByteData;
// todo logger.logcritical no sharedsiteitem found
if (siteItem.SharedSiteItemId.HasValue) byteData = (await repository.SelectSharedSiteItem(siteItem.SharedSiteItemId.Value)).ByteData;
if (siteItem.Path.EndsWith(".html", StringComparison.InvariantCultureIgnoreCase))
{
var html = Encoding.UTF8.GetString(byteData);
return Results.Content(html, "text/html", Encoding.UTF8);
}
else
{
return Results.File(byteData, HttpContenTypeMaps.GetFromPathOrFallback(siteItem.Path));
}
}
#region System pages
public static async Task<IResult> SystemResourceMonitoring([FromServices] IQRepository repository)
{
IEnumerable<ResourceMonitorUtilization> utilization = await repository.SelectResourceMonitorUtilization(50);
var sb = new StringBuilder();
AppendHtmlTable(sb,
new string[] { "id", "date_time", "cpu_used_percentage", "memory_userd_in_bytes", "memory_used_percentage" },
new Func<ResourceMonitorUtilization, object>[]
{
c => c.Id,
c => c.DateTime.ToString("yyyy-MM-dd T HH:mm:ss"),
c => Math.Round(c.CpuUsedPercentage, 2) + "%",
c => Math.Round(c.MemoryUsedInBytes/(double)1000000) + "MB",
c => Math.Round(c.MemoryUsedPercentage, 2) + "%"
},
utilization);
int refreshRate = 5;
#if DEBUG
refreshRate = 1;
#endif
return SimpleHtmlPage($"<meta http-equiv=\"refresh\" content=\"{refreshRate}\">", sb);
}
public static async Task<IResult> SystemStats(
[FromServices] IQRepository repository)
{
var s = await repository.SelectSystemStats();
var sb = new StringBuilder();
sb.AppendFormat("SiteItemCount: {0}", s.SiteItemCount);
sb.AppendLine();
sb.AppendFormat("SharedSiteItemCount: {0}", s.SharedSiteItemCount);
sb.AppendLine();
var sharedUsagePercent = s.SiteItemCountUsingShared == 0 ?
"0" :
Math.Round((double)100 * s.SiteItemCountUsingShared / s.SiteItemCount, 2).ToString();
sb.AppendFormat("SiteItemCountUsingShared: {0} ({1}%)", s.SharedSiteItemCount, sharedUsagePercent);
sb.AppendLine();
sb.AppendFormat("AppLogCount: {0}", s.AppLogCount);
sb.AppendLine();
sb.AppendFormat("HttpLogCount: {0}", s.HttpLogCount);
sb.AppendLine();
sb.AppendFormat("ProjectCount: {0}", s.ProjectCount);
sb.AppendLine();
return Results.Content(sb.ToString(), "text/plain");
}
public static async Task<IResult> SystemSiteItems(
[FromServices] IOptions<DSettings> settings,
[FromServices] IQRepository repository,
[FromRoute] int? pageNo)
{
var siteitems = await repository.GetSiteItemPagedAsync(0, 1000000);
var allprojs = await repository.SelectProjectPagedAsync(0, 100000);
var pdics = allprojs.ToImmutableDictionary(x => x.Id);
var sb = new StringBuilder();
sb.Append("<head></head><body><table>");
sb.Append("<thead><tr> <td>SiteItemId</td> <td>ProjectID</td> <td>Url</td> </tr></thead>");
foreach (var si in siteitems)
{
sb.Append("<tr>");
sb.AppendFormat("<td>{0}</td>", si.Id);
sb.AppendFormat("<td>{0}</td>", si.ProjectId);
sb.Append("<td>");
sb.AppendFormat("<a href=\"{0}\">{0}</a>", FullProjectUrl(settings.Value, pdics[si.ProjectId], si.Path));
sb.Append("</td>");
sb.Append("</tr>");
}
sb.Append("</table></body>");
var result = sb.ToString();
return Results.Content(result, "text/html");
}
public static async Task<IResult> SystemAllProjects(
[FromServices] IOptions<DSettings> settings,
[FromServices] IQRepository repository,
[FromRoute] int? pageNo)
{
var projects = await repository.SelectProjectPagedAsync(pageNo ?? 0, 1000);
if (projects.Length == 0) return Results.Content("no projects", "text/html");
var sb = new StringBuilder();
var td = new Func<Project, string>[]
{
(Project p) => p.Id.ToString(),
(Project p) => p.DnProjectId.ToString(),
(Project p) => p.ProjectType.ToString(),
(Project p) => p.NugetPackageName ?? "",
(Project p) => p.NugetPackageVersion ?? "",
(Project p) => p.UrlPrefix ?? "",
(Project p) => p.ProjectVersion ?? "",
(Project p) => FullProjectUrl(settings.Value, p)
};
int[] tabs = new int[td.Length];
for (int i = 0; i < td.Length; i++)
{
tabs[i] = projects.Max(p => (int)Math.Ceiling((double)td[i](p).Length / 4));
}
sb.Append("<head></head><body>");
sb.Append("<table>");
foreach (var p in projects)
{
// sb.Append("|");
sb.Append("<tr>");
for (int i = 0; i < td.Length; i++)
{
var value = td[i](p);
// var spaces = 4 * tabs[i] - value.Length;
// sb.Append($" {value}{new string(' ', spaces)} |");
if (i != td.Length - 1) { sb.Append("<td>"); sb.Append(value); sb.Append("</td>"); }
else { sb.Append("<td>"); sb.Append($"<a href=\"{value}\">{value}</a>"); sb.Append("</td>"); }
}
sb.Append("<tr>");
// sb.AppendLine();
}
sb.Append("<table>");
sb.Append("</body>");
return Results.Content(sb.ToString(), "text/html");
}
static string FullProjectUrl(DSettings s, Project p, string path = "/api/index.html")
{
if (p.ProjectType == ProjectType.Nuget) return s.GetUrlNugetOrgProject(p.NugetPackageName, p.NugetPackageVersion, path);
else if (p.ProjectType == ProjectType.Singleton) return s.GetUrlSingletonProject(p.UrlPrefix, path);
else return s.GetUrlVersionProject(p.UrlPrefix, p.ProjectVersion, path);
}
#endregion
static IResult SimpleHtmlPage(string headTags, StringBuilder body)
{
StringBuilder sb = new StringBuilder();
string headFormat =
"""
<html>
<head>
{0}
</head>
""";
string bodyFormat =
@"
<body>
<style>
table, th, td {{
border: 1px solid black;
}}
td {{
padding: 8 12px;
}}
table {{
border-collapse: collapse;
}}
</style>
{0}
</body>
<html>
";
sb.AppendFormat(headFormat, headTags ?? "");
sb.AppendFormat(bodyFormat, body);
return Results.Content(sb.ToString(), "text/html");
}
static void AppendHtmlTable<T>(StringBuilder sb, string[] columns, Func<T, object>[] config, IEnumerable<T> values)
{
if (columns.Length != config.Length) throw new ArgumentException("config.lenth != columns.length");
sb.Append("<table>");
sb.Append("<thead><tr>");
foreach (var item in columns)
{
sb.AppendFormat("<td>{0}</td>", item);
}
sb.Append("</tr></thead><tbody>");
foreach (var item in values)
{
sb.Append("<tr>");
for (int i = 0; i < columns.Length; i++)
{
sb.AppendFormat("<td>{0}</td>", config[i](item));
}
sb.Append("</tr>");
}
sb.Append("</tbody></table>");
}
}
}