Nice little directory browser :D
at master 459 lines 16 kB view raw
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}