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")
37 ?? throw new NullReferenceException("env nhnd_Utatane_ROOT not set!");
38
39 public static List<String> DoNotServe = ["*cookies.txt"];
40
41 private static readonly HtmlSettings HtmlSettings = new HtmlSettings() {
42 RemoveComments = false,
43 RemoveOptionalTags = false,
44 RemoveInvalidClosingTags = false,
45 RemoveEmptyAttributes = false,
46 RemoveScriptStyleTypeAttribute = false,
47 ShortBooleanAttribute = false,
48 IsFragmentOnly = true,
49 MinifyJs = false,
50 MinifyJsAttributes = false,
51 MinifyCss = false,
52 MinifyCssAttributes = false,
53 };
54
55 public static String OptimizeHtml(String html) {
56 return Uglify.Html(html, HtmlSettings).Code ?? String.Empty;
57 }
58
59 public static Result<OneResult<DirectoryInfo, FileInfo>> VerifyPath(String path) {
60 if (DoNotServe.Contains(path))
61 return Result.Fail($"Do Not Serve: {path}");
62
63 String fullPath = Path.GetFullPath(Path.Join(Root, path));
64
65 if (!fullPath.StartsWith(Root))
66 return Result.Fail($"Outside root: {path} -> {fullPath}");
67
68 var maybeDir = new DirectoryInfo(fullPath);
69 if (maybeDir.Exists)
70 return Result.Ok<OneResult<DirectoryInfo, FileInfo>>(maybeDir);
71
72 var maybeFile = new FileInfo(fullPath);
73 if (maybeFile.Exists)
74 return Result.Ok<OneResult<DirectoryInfo, FileInfo>>(maybeFile);
75
76 return Result.Fail($"Does not exist: {path} -> {fullPath}");
77 }
78
79 public static String FormatFileSize(Int64 size) {
80 const Int64 terabyte = 1099511627776; // 2**40
81 const Int64 gigabyte = 1073741824; // 2**30
82 const Int64 megabyte = 1048576; // 2**20
83 const Int64 kilobyte = 1024; // 2**10
84
85 double sizeD = size;
86
87 return size switch {
88 > terabyte => $"{(sizeD / terabyte):F2} TiB ",
89 > gigabyte => $"{(sizeD / gigabyte):F2} GiB",
90 > megabyte => $"{(sizeD / megabyte):F2} MiB",
91 > kilobyte => $"{(sizeD / kilobyte):F2} KiB",
92 _ => $"{size} B"
93 };
94 }
95
96 public static MarkupString AbbrIcon(String text, String symbol) {
97 return (MarkupString)AbbrIconPlain(text, symbol);
98 }
99
100 public static String AbbrIconPlain(String text, String symbol) {
101 var abbr = new TagBuilder("abbr");
102 abbr.Attributes.Add("title", text);
103 abbr.InnerHtml.Append(symbol);
104
105 using var writer = new StringWriter();
106 abbr.WriteTo(writer, System.Text.Encodings.Web.HtmlEncoder.Default);
107
108 return writer.ToString();
109 }
110
111 private static readonly Dictionary<String, MarkupString> ExtToAbbr = new() {
112 #region Extension <--> Icon mappings
113 { ".avc", AbbrIcon("Video file", "🎞️") },
114 { ".flv", AbbrIcon("Video file", "🎞️") },
115 { ".mts", AbbrIcon("Video file", "🎞️") },
116 { ".m2ts", AbbrIcon("Video file", "🎞️") },
117 { ".m4v", AbbrIcon("Video file", "🎞️") },
118 { ".mkv", AbbrIcon("Video file", "🎞️") },
119 { ".mov", AbbrIcon("Video file", "🎞️") },
120 { ".mp4", AbbrIcon("Video file", "🎞️") },
121 { ".ts", AbbrIcon("Video file", "🎞️") },
122 { ".webm", AbbrIcon("Video file", "🎞️") },
123 { ".wmv", AbbrIcon("Video file", "🎞️") },
124
125 { ".aac", AbbrIcon("Audio file", "🔊") },
126 { ".alac", AbbrIcon("Audio file", "🔊") },
127 { ".flac", AbbrIcon("Audio file", "🔊") },
128 { ".m4a", AbbrIcon("Audio file", "🔊") },
129 { ".mp3", AbbrIcon("Audio file", "🔊") },
130 { ".opus", AbbrIcon("Audio file", "🔊") },
131 { ".wav", AbbrIcon("Audio file", "🔊") },
132 { ".ogg", AbbrIcon("Audio file", "🔊") },
133 { ".mus", AbbrIcon("Audio file", "🔊") },
134
135 { ".avif", AbbrIcon("Image file", "🖼️") },
136 { ".bmp", AbbrIcon("Image file", "🖼️") },
137 { ".gif", AbbrIcon("Image file", "🖼️") },
138 { ".ico", AbbrIcon("Image file", "🖼️") },
139 { ".heic", AbbrIcon("Image file", "🖼️") },
140 { ".heif", AbbrIcon("Image file", "🖼️") },
141 { ".jpe?g", AbbrIcon("Image file", "🖼️") },
142 { ".jfif", AbbrIcon("Image file", "🖼️") },
143 { ".jxl", AbbrIcon("Image file", "🖼️") },
144 { ".j2c", AbbrIcon("Image file", "🖼️") },
145 { ".jp2", AbbrIcon("Image file", "🖼️") },
146 { ".a?png", AbbrIcon("Image file", "🖼️") },
147 { ".svg", AbbrIcon("Image file", "🖼️") },
148 { ".tiff?", AbbrIcon("Image file", "🖼️") },
149 { ".webp", AbbrIcon("Image file", "🖼️") },
150 { ".pdn", AbbrIcon("Image file", "🖼️") },
151 { ".psd", AbbrIcon("Image file", "🖼️") },
152 { ".xcf", AbbrIcon("Image file", "🖼️") },
153
154 { ".ass", AbbrIcon("Subtitle file", "💬") },
155 { ".lrc", AbbrIcon("Subtitle file", "💬") },
156 { ".srt", AbbrIcon("Subtitle file", "💬") },
157 { ".srv3", AbbrIcon("Subtitle file", "💬") },
158 { ".ssa", AbbrIcon("Subtitle file", "💬") },
159 { ".vtt", AbbrIcon("Subtitle file", "💬") },
160
161 { ".bat", AbbrIcon("Windows script file", "📜") },
162 { ".cmd", AbbrIcon("Windows script file", "📜") },
163 { ".htm", AbbrIcon("HTML file", "📜") },
164 { ".html", AbbrIcon("HTML file", "📜") },
165 { ".xhtml", AbbrIcon("XHTML file", "📜") },
166 { ".bash", AbbrIcon("Shell script", "📜") },
167 { ".zsh", AbbrIcon("Shell script", "📜") },
168 { ".sh", AbbrIcon("Shell script", "📜") },
169 { ".cpp", AbbrIcon("C++ source file", "📜") },
170 { ".cxx", AbbrIcon("C++ source file", "📜") },
171 { ".cc", AbbrIcon("C++ source file", "📜") },
172 { ".hpp", AbbrIcon("C++ header file", "📜") },
173 { ".hxx", AbbrIcon("C++ header file", "📜") },
174 { ".hh", AbbrIcon("C++ header file", "📜") },
175
176 { ".py", AbbrIcon("Python script", "📜") },
177 { ".pyc", AbbrIcon("Compiled Python bytecode", "📜") },
178 { ".pyo", AbbrIcon("Compiled Python bytecode", "📜") },
179 { ".psm1", AbbrIcon("PowerShell module file", "📜") },
180 { ".psd1", AbbrIcon("PowerShell data file", "📜") },
181 { ".ps1", AbbrIcon("PowerShell script", "📜") },
182 { ".js", AbbrIcon("JavaScript source code", "📜") },
183 { ".css", AbbrIcon("CSS style sheet", "📜") },
184 { ".cs", AbbrIcon("C# source file", "📜") },
185 { ".c", AbbrIcon("C source file", "📜") },
186 { ".h", AbbrIcon("C header file", "📜") },
187 { ".java", AbbrIcon("Java source file", "📜") },
188
189 { ".json", AbbrIcon("Data/config file", "📜") },
190 { ".json5", AbbrIcon("Data/config file", "📜") },
191 { ".xml", AbbrIcon("Data/config file", "📜") },
192 { ".yaml", AbbrIcon("Data/config file", "📜") },
193 { ".yml", AbbrIcon("Data/config file", "📜") },
194 { ".ini", AbbrIcon("Data/config file", "📜") },
195 { ".toml", AbbrIcon("Data/config file", "📜") },
196 { ".cfg", AbbrIcon("Data/config file", "📜") },
197 { ".conf", AbbrIcon("Data/config file", "📜") },
198 { ".plist", AbbrIcon("Data/config file", "📜") },
199 { ".csv", AbbrIcon("Data/config file", "📜") },
200
201 { ".tar", AbbrIcon("File archive", "📦") },
202 { ".ar", AbbrIcon("File archive", "📦") },
203 { ".7z", AbbrIcon("File archive", "📦") },
204 { ".arc", AbbrIcon("File archive", "📦") },
205 { ".cab", AbbrIcon("File archive", "📦") },
206 { ".rar", AbbrIcon("File archive", "📦") },
207 { ".zip", AbbrIcon("File archive", "📦") },
208 { ".bz2", AbbrIcon("File archive", "📦") },
209 { ".gz", AbbrIcon("File archive", "📦") },
210 { ".lz", AbbrIcon("File archive", "📦") },
211 { ".lzma", AbbrIcon("File archive", "📦") },
212 { ".lzo", AbbrIcon("File archive", "📦") },
213 { ".xz", AbbrIcon("File archive", "📦") },
214 { ".Z", AbbrIcon("File archive", "📦") },
215 { ".zst", AbbrIcon("File archive", "📦") },
216
217 { ".apk", AbbrIcon("Android package", "📦") },
218 { ".deb", AbbrIcon("Debian package", "📦") },
219 { ".rpm", AbbrIcon("RPM package", "📦") },
220 { ".ipa", AbbrIcon("iOS/iPadOS package", "📦") },
221 { ".AppImage", AbbrIcon("AppImage bundle", "📦") },
222 { ".jar", AbbrIcon("Java archive", "☕") },
223
224 { ".dmg", AbbrIcon("Disk image", "💿") },
225 { ".iso", AbbrIcon("Disk image", "💿") },
226 { ".img", AbbrIcon("Disk image", "💿") },
227 { ".wim", AbbrIcon("Disk image", "💿") },
228 { ".esd", AbbrIcon("Disk image", "💿") },
229
230
231 { ".docx", AbbrIcon("Document", "📃") },
232 { ".doc", AbbrIcon("Document", "📃") },
233 { ".odt", AbbrIcon("Document", "📃") },
234 { ".pptx", AbbrIcon("Presentation", "📃") },
235 { ".ppt", AbbrIcon("Presentation", "📃") },
236 { ".odp", AbbrIcon("Presentation", "📃") },
237 { ".xslx", AbbrIcon("Spreadsheet", "📃") },
238 { ".xsl", AbbrIcon("Spreadsheet", "📃") },
239 { ".ods", AbbrIcon("Spreadsheet", "📃") },
240 { ".pdf", AbbrIcon("PDF", "📃") },
241 { ".md", AbbrIcon("Markdown document", "📃") },
242 { ".rst", AbbrIcon("reStructuredText document", "📃") },
243 { ".epub", AbbrIcon("EPUB e-book file", "📃") },
244 { ".log", AbbrIcon("Log file", "📃") },
245 { ".txt", AbbrIcon("Text file", "📃") },
246
247 { ".tff", AbbrIcon("Font file", "🗛") },
248 { ".otf", AbbrIcon("Font file", "🗛") },
249 { ".woff", AbbrIcon("Font file", "🗛") },
250 { ".woff2", AbbrIcon("Font file", "🗛") },
251
252 { ".mpls", AbbrIcon("Playlist file", "🎶") },
253 { ".m3u", AbbrIcon("Playlist file", "🎶") },
254 { ".m3u8", AbbrIcon("Playlist file", "🎶") },
255
256 { ".exe", AbbrIcon("Generic executable", "🔳") },
257 { ".elf", AbbrIcon("Generic executable", "🔳") },
258 { ".msi", AbbrIcon("Generic executable", "🔳") },
259 { ".msix", AbbrIcon("Generic executable", "🔳") },
260 { ".msixbundle", AbbrIcon("Generic executable", "🔳") },
261 { ".appx", AbbrIcon("Generic executable", "🔳") },
262 { ".appxbundle", AbbrIcon("Generic executable", "🔳") },
263
264 { ".dll", AbbrIcon("Dynamic library", "⚙️") },
265 { ".so", AbbrIcon("Dynamic library", "⚙️") },
266 { ".dylib", AbbrIcon("Dynamic library", "⚙️") }
267 #endregion
268 };
269
270 public static MarkupString GetIconForFileType(FileSystemInfo fsi) {
271 if (fsi is DirectoryInfo) {
272 return AbbrIcon("Directory", "📁");
273 }
274
275 if (ExtToAbbr.TryGetValue(fsi.Extension, out var abbr))
276 return abbr;
277
278 return fsi.EuidAccess_R_X()
279 ? AbbrIcon("Generic executable (+x)", "🔳")
280 : AbbrIcon("File", "📄");
281 }
282
283 extension<T>(IEnumerable<T> enumerable) {
284 public T RandomElement() {
285 var list = enumerable.ToList();
286 int index = (new Random()).Next(0, list.Count);
287 return list.ElementAt(index);
288 }
289 }
290
291#pragma warning disable CA2101
292 // DO NOT mark these as `CharSet = CharSet.Unicode`, this will break things!!
293 [DllImport("libc.so.6")]
294 private static extern int euidaccess(String pathname, int mode);
295#pragma warning restore CA2101
296
297 private const int R_OK = 0b0100;
298 private const int W_OK = 0b0010;
299 private const int X_OK = 0b0001;
300
301 extension(FileSystemInfo fsi) {
302 public bool EuidAccess_R() {
303 if (OperatingSystem.IsWindows()) {
304 throw new NotImplementedException("No libc (euidaccess) on Windows!");
305 }
306
307 return euidaccess(fsi.FullName, R_OK) == 0;
308 }
309
310 public bool EuidAccess_R_X() {
311 if (OperatingSystem.IsWindows()) {
312 throw new NotImplementedException("No libc (euidaccess) on Windows!");
313 }
314
315 return euidaccess(fsi.FullName, R_OK | X_OK) == 0;
316 }
317
318 public bool IsReadable() {
319 if (!fsi.Exists) {
320 return false;
321 }
322
323 if (fsi is DirectoryInfo dir) {
324 return dir.EuidAccess_R_X();
325 }
326
327 return fsi.EuidAccess_R();
328 }
329
330 // wrapper(win)/replacement(*nix) for ResolveLinkTarget
331 // if not link, return self
332 // see https://github.com/PowerShell/PowerShell/issues/25724
333 public FileSystemInfo? ReadLink() {
334 if (!fsi.Attributes.HasFlag(FileAttributes.ReparsePoint))
335 return null;
336
337 if (OperatingSystem.IsWindows())
338 return fsi.ResolveLinkTarget(true);
339
340 Process rp = new Process {
341 StartInfo = new ProcessStartInfo("realpath", ["-m", fsi.FullName]) {
342 UseShellExecute = false,
343 RedirectStandardOutput = true
344 }
345 };
346
347 rp.Start();
348 var real = rp.StandardOutput.ReadLine();
349 rp.WaitForExit();
350
351 if (Directory.Exists(real))
352 return new DirectoryInfo(real);
353
354 if (File.Exists(real))
355 return new FileInfo(real);
356
357 throw new FileNotFoundException($"Link target for {fsi.FullName} -> {fsi.LinkTarget} not found.");
358 }
359
360 public FileSystemInfo UnravelLink() {
361 FileSystemInfo here = fsi ?? throw new ArgumentNullException(nameof(fsi));
362 FileSystemInfo? next;
363
364 while (true) {
365 // let the exception propagate
366 next = here.ReadLink();
367
368 if (next == null)
369 return here;
370
371 here = next;
372 }
373 }
374 }
375
376 extension(RenderFragment fragment) {
377 public String RenderString() {
378 StringBuilder builder = new StringBuilder();
379 RenderTreeBuilder renderer = new RenderTreeBuilder();
380 fragment(renderer);
381
382 renderer.GetFrames().Array.ToList()
383 .ForEach(f => {
384 // this seems like the only types that have actual content?
385 // idk tho
386 switch (f.FrameType) {
387 case RenderTreeFrameType.Markup:
388 builder.Append(f.MarkupContent);
389 break;
390 case RenderTreeFrameType.Text:
391 builder.Append(f.TextContent);
392 break;
393 }
394 });
395
396 return builder.ToString();
397 }
398 }
399
400 extension(String str) {
401 public String ReplaceRegex(String regex, String replacement) {
402 return Regex.Replace(str, regex, replacement);
403 }
404
405 public String ReplaceRegex(String regex, MatchEvaluator replacement) {
406 return Regex.Replace(str, regex, replacement);
407 }
408
409 public bool Matches(String regex) {
410 return Regex.IsMatch(str, regex);
411 }
412 }
413}