Nice little directory browser :D
at master 413 lines 14 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") 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}