Nice little directory browser :D
1/*
2 This file is part of Utatane.
3
4 Utatane is free software: you can redistribute it and/or modify it under
5 the terms of the GNU Affero General Public License as published by the Free
6 Software Foundation, either version 3 of the License, or (at your option)
7 any later version.
8
9 Utatane is distributed in the hope that it will be useful, but WITHOUT ANY
10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
12 more details.
13
14 You should have received a copy of the GNU Affero General Public License
15 along with Utatane. If not, see <http://www.gnu.org/licenses/>.
16*/
17
18#pragma warning disable BL0006
19
20using System.Diagnostics;
21using System.Runtime.InteropServices;
22using System.Text;
23using System.Text.RegularExpressions;
24using FluentResults;
25using Functional.DiscriminatedUnion;
26using Microsoft.AspNetCore.Components;
27using Microsoft.AspNetCore.Components.Rendering;
28using Microsoft.AspNetCore.Components.RenderTree;
29using Microsoft.AspNetCore.Mvc.Rendering;
30using NUglify;
31using NUglify.Html;
32
33namespace Utatane;
34
35public static class Utils {
36 public static String Root = Environment.GetEnvironmentVariable("nhnd_Utatane_ROOT") ?? throw new NullReferenceException("env nhnd_Utatane_ROOT not set!");
37
38 public static List<String> DoNotServe = ["*cookies.txt"];
39
40 private static readonly HtmlSettings HtmlSettings = new HtmlSettings() {
41 RemoveComments = false,
42 RemoveOptionalTags = false,
43 RemoveInvalidClosingTags = false,
44 RemoveEmptyAttributes = false,
45 RemoveScriptStyleTypeAttribute = false,
46 ShortBooleanAttribute = false,
47 IsFragmentOnly = true,
48 MinifyJs = false,
49 MinifyJsAttributes = false,
50 MinifyCss = false,
51 MinifyCssAttributes = false,
52 };
53
54 public static String OptimizeHtml(String html) {
55 return Uglify.Html(html, HtmlSettings).Code ?? String.Empty;
56 }
57
58 public static Result<OneResult<DirectoryInfo, FileInfo>> VerifyPath(String path) {
59 if (DoNotServe.Contains(path))
60 return Result.Fail($"Do Not Serve: {path}");
61
62 String fullPath = Path.GetFullPath(Path.Join(Root, path));
63
64 if (!fullPath.StartsWith(Root))
65 return Result.Fail($"Outside root: {path} -> {fullPath}");
66
67 var maybeDir = new DirectoryInfo(fullPath);
68 if (maybeDir.Exists)
69 return Result.Ok<OneResult<DirectoryInfo, FileInfo>>(maybeDir);
70
71 var maybeFile = new FileInfo(fullPath);
72 if (maybeFile.Exists)
73 return Result.Ok<OneResult<DirectoryInfo, FileInfo>>(maybeFile);
74
75 return Result.Fail($"Does not exist: {path} -> {fullPath}");
76 }
77
78 public static String FormatFileSize(Int64 size) {
79 const Int64 terabyte = 1099511627776; // 2**40
80 const Int64 gigabyte = 1073741824; // 2**30
81 const Int64 megabyte = 1048576; // 2**20
82 const Int64 kilobyte = 1024; // 2**10
83
84 double sizeD = size;
85
86 return size switch {
87 > terabyte => $"{(sizeD / terabyte):F2} TiB ",
88 > gigabyte => $"{(sizeD / gigabyte):F2} GiB",
89 > megabyte => $"{(sizeD / megabyte):F2} MiB",
90 > kilobyte => $"{(sizeD / kilobyte):F2} KiB",
91 _ => $"{size} B"
92 };
93 }
94
95 public static MarkupString AbbrIcon(String text, String symbol) {
96 return (MarkupString)AbbrIconPlain(text, symbol);
97 }
98
99 public static String AbbrIconPlain(String text, String symbol) {
100 RenderTreeBuilder renderer = new RenderTreeBuilder();
101
102 var abbr = new TagBuilder("abbr");
103 abbr.Attributes.Add("title", text);
104 abbr.InnerHtml.Append(symbol);
105
106 using var writer = new System.IO.StringWriter();
107 abbr.WriteTo(writer, System.Text.Encodings.Web.HtmlEncoder.Default);
108
109 return writer.ToString();
110 }
111
112 private static readonly Dictionary<String, MarkupString> ExtToAbbr = new Dictionary<String, MarkupString> {
113 #region Extension <--> Icon mappings
114 { ".avc", AbbrIcon("Video file", "🎞️") },
115 { ".flv", AbbrIcon("Video file", "🎞️") },
116 { ".mts", AbbrIcon("Video file", "🎞️") },
117 { ".m2ts", AbbrIcon("Video file", "🎞️") },
118 { ".m4v", AbbrIcon("Video file", "🎞️") },
119 { ".mkv", AbbrIcon("Video file", "🎞️") },
120 { ".mov", AbbrIcon("Video file", "🎞️") },
121 { ".mp4", AbbrIcon("Video file", "🎞️") },
122 { ".ts", AbbrIcon("Video file", "🎞️") },
123 { ".webm", AbbrIcon("Video file", "🎞️") },
124 { ".wmv", AbbrIcon("Video file", "🎞️") },
125
126 { ".aac", AbbrIcon("Audio file", "🔊") },
127 { ".alac", AbbrIcon("Audio file", "🔊") },
128 { ".flac", AbbrIcon("Audio file", "🔊") },
129 { ".m4a", AbbrIcon("Audio file", "🔊") },
130 { ".mp3", AbbrIcon("Audio file", "🔊") },
131 { ".opus", AbbrIcon("Audio file", "🔊") },
132 { ".wav", AbbrIcon("Audio file", "🔊") },
133 { ".ogg", AbbrIcon("Audio file", "🔊") },
134 { ".mus", AbbrIcon("Audio file", "🔊") },
135
136 { ".avif", AbbrIcon("Image file", "🖼️") },
137 { ".bmp", AbbrIcon("Image file", "🖼️") },
138 { ".gif", AbbrIcon("Image file", "🖼️") },
139 { ".ico", AbbrIcon("Image file", "🖼️") },
140 { ".heic", AbbrIcon("Image file", "🖼️") },
141 { ".heif", AbbrIcon("Image file", "🖼️") },
142 { ".jpe?g", AbbrIcon("Image file", "🖼️") },
143 { ".jfif", AbbrIcon("Image file", "🖼️") },
144 { ".jxl", AbbrIcon("Image file", "🖼️") },
145 { ".j2c", AbbrIcon("Image file", "🖼️") },
146 { ".jp2", AbbrIcon("Image file", "🖼️") },
147 { ".a?png", AbbrIcon("Image file", "🖼️") },
148 { ".svg", AbbrIcon("Image file", "🖼️") },
149 { ".tiff?", AbbrIcon("Image file", "🖼️") },
150 { ".webp", AbbrIcon("Image file", "🖼️") },
151 { ".pdn", AbbrIcon("Image file", "🖼️") },
152 { ".psd", AbbrIcon("Image file", "🖼️") },
153 { ".xcf", AbbrIcon("Image file", "🖼️") },
154
155 { ".ass", AbbrIcon("Subtitle file", "💬") },
156 { ".lrc", AbbrIcon("Subtitle file", "💬") },
157 { ".srt", AbbrIcon("Subtitle file", "💬") },
158 { ".srv3", AbbrIcon("Subtitle file", "💬") },
159 { ".ssa", AbbrIcon("Subtitle file", "💬") },
160 { ".vtt", AbbrIcon("Subtitle file", "💬") },
161
162 { ".bat", AbbrIcon("Windows script file", "📜") },
163 { ".cmd", AbbrIcon("Windows script file", "📜") },
164 { ".htm", AbbrIcon("HTML file", "📜") },
165 { ".html", AbbrIcon("HTML file", "📜") },
166 { ".xhtml", AbbrIcon("XHTML file", "📜") },
167 { ".bash", AbbrIcon("Shell script", "📜") },
168 { ".zsh", AbbrIcon("Shell script", "📜") },
169 { ".sh", AbbrIcon("Shell script", "📜") },
170 { ".cpp", AbbrIcon("C++ source file", "📜") },
171 { ".cxx", AbbrIcon("C++ source file", "📜") },
172 { ".cc", AbbrIcon("C++ source file", "📜") },
173 { ".hpp", AbbrIcon("C++ header file", "📜") },
174 { ".hxx", AbbrIcon("C++ header file", "📜") },
175 { ".hh", AbbrIcon("C++ header file", "📜") },
176
177 { ".py", AbbrIcon("Python script", "📜") },
178 { ".pyc", AbbrIcon("Compiled Python bytecode", "📜") },
179 { ".pyo", AbbrIcon("Compiled Python bytecode", "📜") },
180 { ".psm1", AbbrIcon("PowerShell module file", "📜") },
181 { ".psd1", AbbrIcon("PowerShell data file", "📜") },
182 { ".ps1", AbbrIcon("PowerShell script", "📜") },
183 { ".js", AbbrIcon("JavaScript source code", "📜") },
184 { ".css", AbbrIcon("CSS style sheet", "📜") },
185 { ".cs", AbbrIcon("C# source file", "📜") },
186 { ".c", AbbrIcon("C source file", "📜") },
187 { ".h", AbbrIcon("C header file", "📜") },
188 { ".java", AbbrIcon("Java source file", "📜") },
189
190 { ".json", AbbrIcon("Data/config file", "📜") },
191 { ".json5", AbbrIcon("Data/config file", "📜") },
192 { ".xml", AbbrIcon("Data/config file", "📜") },
193 { ".yaml", AbbrIcon("Data/config file", "📜") },
194 { ".yml", AbbrIcon("Data/config file", "📜") },
195 { ".ini", AbbrIcon("Data/config file", "📜") },
196 { ".toml", AbbrIcon("Data/config file", "📜") },
197 { ".cfg", AbbrIcon("Data/config file", "📜") },
198 { ".conf", AbbrIcon("Data/config file", "📜") },
199 { ".plist", AbbrIcon("Data/config file", "📜") },
200 { ".csv", AbbrIcon("Data/config file", "📜") },
201
202 { ".tar", AbbrIcon("File archive", "📦") },
203 { ".ar", AbbrIcon("File archive", "📦") },
204 { ".7z", AbbrIcon("File archive", "📦") },
205 { ".arc", AbbrIcon("File archive", "📦") },
206 { ".cab", AbbrIcon("File archive", "📦") },
207 { ".rar", AbbrIcon("File archive", "📦") },
208 { ".zip", AbbrIcon("File archive", "📦") },
209 { ".bz2", AbbrIcon("File archive", "📦") },
210 { ".gz", AbbrIcon("File archive", "📦") },
211 { ".lz", AbbrIcon("File archive", "📦") },
212 { ".lzma", AbbrIcon("File archive", "📦") },
213 { ".lzo", AbbrIcon("File archive", "📦") },
214 { ".xz", AbbrIcon("File archive", "📦") },
215 { ".Z", AbbrIcon("File archive", "📦") },
216 { ".zst", AbbrIcon("File archive", "📦") },
217
218 { ".apk", AbbrIcon("Android package", "📦") },
219 { ".deb", AbbrIcon("Debian package", "📦") },
220 { ".rpm", AbbrIcon("RPM package", "📦") },
221 { ".ipa", AbbrIcon("iOS/iPadOS package", "📦") },
222 { ".AppImage", AbbrIcon("AppImage bundle", "📦") },
223 { ".jar", AbbrIcon("Java archive", "☕") },
224
225 { ".dmg", AbbrIcon("Disk image", "💿") },
226 { ".iso", AbbrIcon("Disk image", "💿") },
227 { ".img", AbbrIcon("Disk image", "💿") },
228 { ".wim", AbbrIcon("Disk image", "💿") },
229 { ".esd", AbbrIcon("Disk image", "💿") },
230
231
232 { ".docx", AbbrIcon("Document", "📃") },
233 { ".doc", AbbrIcon("Document", "📃") },
234 { ".odt", AbbrIcon("Document", "📃") },
235 { ".pptx", AbbrIcon("Presentation", "📃") },
236 { ".ppt", AbbrIcon("Presentation", "📃") },
237 { ".odp", AbbrIcon("Presentation", "📃") },
238 { ".xslx", AbbrIcon("Spreadsheet", "📃") },
239 { ".xsl", AbbrIcon("Spreadsheet", "📃") },
240 { ".ods", AbbrIcon("Spreadsheet", "📃") },
241 { ".pdf", AbbrIcon("PDF", "📃") },
242 { ".md", AbbrIcon("Markdown document", "📃") },
243 { ".rst", AbbrIcon("reStructuredText document", "📃") },
244 { ".epub", AbbrIcon("EPUB e-book file", "📃") },
245 { ".log", AbbrIcon("Log file", "📃") },
246 { ".txt", AbbrIcon("Text file", "📃") },
247
248 { ".tff", AbbrIcon("Font file", "🗛") },
249 { ".otf", AbbrIcon("Font file", "🗛") },
250 { ".woff", AbbrIcon("Font file", "🗛") },
251 { ".woff2", AbbrIcon("Font file", "🗛") },
252
253 { ".mpls", AbbrIcon("Playlist file", "🎶") },
254 { ".m3u", AbbrIcon("Playlist file", "🎶") },
255 { ".m3u8", AbbrIcon("Playlist file", "🎶") },
256
257 { ".exe", AbbrIcon("Generic executable", "🔳") },
258 { ".elf", AbbrIcon("Generic executable", "🔳") },
259 { ".msi", AbbrIcon("Generic executable", "🔳") },
260 { ".msix", AbbrIcon("Generic executable", "🔳") },
261 { ".msixbundle", AbbrIcon("Generic executable", "🔳") },
262 { ".appx", AbbrIcon("Generic executable", "🔳") },
263 { ".appxbundle", AbbrIcon("Generic executable", "🔳") },
264
265 { ".dll", AbbrIcon("Dynamic library", "⚙️") },
266 { ".so", AbbrIcon("Dynamic library", "⚙️") },
267 { ".dylib", AbbrIcon("Dynamic library", "⚙️") }
268 #endregion
269 };
270
271 public static MarkupString GetIconForFileType(FileSystemInfo fsi) {
272 if (fsi is DirectoryInfo dir) {
273 return AbbrIcon("Directory", "📁");
274 }
275
276 if (ExtToAbbr.TryGetValue(fsi.Name, out var abbr))
277 return ExtToAbbr[fsi.Extension];
278
279 return fsi.EuidAccess_R_X()
280 ? AbbrIcon("Generic executable (+x)", "🔳")
281 : AbbrIcon("File", "📄");
282 }
283
284 extension<T>(IEnumerable<T> enumerable) {
285 public T RandomElement() {
286 var list = enumerable.ToList();
287 int index = (new Random()).Next(0, list.Count);
288 return list.ElementAt(index);
289 }
290 }
291
292#pragma warning disable CA2101
293 // DO NOT mark these as `CharSet = CharSet.Unicode`, this will break things!!
294 [DllImport("libc.so.6")]
295 private static extern int euidaccess(String pathname, int mode);
296
297 [DllImport("libc.so.6", SetLastError=true)]
298 private static extern String? realpath(String pathname, IntPtr resolved);
299#pragma warning restore CA2101
300
301 private const int R_OK = 0b0100;
302 private const int W_OK = 0b0010;
303 private const int X_OK = 0b0001;
304
305 extension(FileSystemInfo fsi) {
306 public bool EuidAccess_R() {
307 if (OperatingSystem.IsWindows()) {
308 throw new NotImplementedException("No libc (euidaccess) on Windows!");
309 }
310
311 return euidaccess(fsi.FullName, R_OK) == 0;
312 }
313
314 public bool EuidAccess_R_X() {
315 if (OperatingSystem.IsWindows()) {
316 throw new NotImplementedException("No libc (euidaccess) on Windows!");
317 }
318
319 return euidaccess(fsi.FullName, R_OK | X_OK) == 0;
320 }
321
322 public bool IsReadable() {
323 if (!fsi.Exists) {
324 return false;
325 }
326
327 if (fsi is DirectoryInfo dir) {
328 return dir.EuidAccess_R_X();
329 }
330
331 return fsi.EuidAccess_R();
332 }
333
334 // wrapper(win)/replacement(*nix) for ResolveLinkTarget
335 // if not link, return self
336 // see https://github.com/PowerShell/PowerShell/issues/25724
337 public FileSystemInfo? ReadLink() {
338 if (!fsi.Attributes.HasFlag(FileAttributes.ReparsePoint)) {
339 return null;
340 }
341
342 if (OperatingSystem.IsWindows()) {
343 return fsi.ResolveLinkTarget(true);
344 }
345
346 Process rp = new Process {
347 StartInfo = new ProcessStartInfo("realpath", ["-m", fsi.FullName]) {
348 UseShellExecute = false,
349 RedirectStandardOutput = true
350 }
351 };
352
353 rp.Start();
354 var real = rp.StandardOutput.ReadLine();
355 rp.WaitForExit();
356
357 if (Directory.Exists(real)) {
358 return new DirectoryInfo(real);
359 }
360
361 // the file at real might not actually exist, but we want to return it anyways
362 return new FileInfo(real);
363 }
364
365 // TODO: uhh this might loop if the target doesnt exist
366 public FileSystemInfo UnravelLink() {
367 FileSystemInfo floor = fsi ?? throw new ArgumentNullException(nameof(fsi));
368
369 while (true) {
370 if (floor!.ReadLink() != null) {
371 floor = floor!.ReadLink()!;
372 } else {
373 return floor;
374 }
375 }
376
377 }
378 }
379
380 extension(RenderFragment fragment) {
381 public String RenderString() {
382 StringBuilder builder = new StringBuilder();
383 RenderTreeBuilder renderer = new RenderTreeBuilder();
384 fragment(renderer);
385
386 renderer.GetFrames().Array.ToList()
387 .ForEach(f => {
388 // this seems like the only types that have actual content?
389 // idk tho
390 switch (f.FrameType) {
391 case RenderTreeFrameType.Markup:
392 builder.Append(f.MarkupContent);
393 break;
394 case RenderTreeFrameType.Text:
395 builder.Append(f.TextContent);
396 break;
397 default:
398 break;
399 }
400 });
401
402 return builder.ToString();
403 }
404 }
405
406 extension(String str) {
407 public String ReplaceRegex(String regex, String replacement) {
408 return Regex.Replace(str, regex, replacement);
409 }
410
411 public String ReplaceRegex(String regex, MatchEvaluator replacement) {
412 return Regex.Replace(str, regex, replacement);
413 }
414
415 public bool Matches(String regex) {
416 return Regex.IsMatch(str, regex);
417 }
418 }
419
420 // extension(OneResult<T1> oneResult) {
421 // public bool Is<Tx>() where Tx : T1 {
422 // return typeof(Tx) is typeof(T1) ? oneResult.IsT1 : false;
423 // }
424 //
425 // public Tx As<Tx>() where Tx : T1 {
426 // return oneResult.IsT1 ? (Tx)oneResult.AsT1() : throw new InvalidOperationException($"Cannot cast value to type {nameof(Tx)}");
427 // }
428 // }
429 //
430 // extension(OneResult<T1, T2> oneResult) {
431 // public bool Is<Tx>() where Tx : T2 {
432 // return typeof(Tx) is typeof(T2) ? oneResult.IsT2 : false;
433 // }
434 //
435 // public Tx As<Tx>() where Tx : T2 {
436 // return oneResult.IsT2 ? (Tx)oneResult.AsT2() : throw new InvalidOperationException($"Cannot cast value to type {nameof(Tx)}");
437 // }
438 // }
439 //
440 // extension(OneResult<T1, T2, T3> oneResult) {
441 // public bool Is<Tx>() where Tx : T3 {
442 // return typeof(Tx) is typeof(T3) ? oneResult.IsT3 : false;
443 // }
444 //
445 // public Tx As<Tx>() where Tx : T3 {
446 // return oneResult.IsT3 ? (Tx)oneResult.AsT3() : throw new InvalidOperationException($"Cannot cast value to type {nameof(Tx)}");
447 // }
448 // }
449 //
450 // extension(OneResult<T1, T2, T3, T4> oneResult) {
451 // public bool Is(Type tx) where Tx : T4 {
452 // return typeof(Tx) is typeof(T4) ? oneResult.IsT4 : false;
453 // }
454 //
455 // public Tx As<Tx>() where Tx : T4 {
456 // return oneResult.IsT4 ? (Tx)oneResult.AsT4() : throw new InvalidOperationException($"Cannot cast value to type {nameof(Tx)}");
457 // }
458 // }
459}