Nice little directory browser :D

Blazor rewrite!! (the other one) (-in-progress)

following the lead of NKK, this project is also switching to Blazor, away from Pode for similar reasons
* basic directory listing works
* no sorting (very easy, soon)
* does not serve files just yet
* wow this was easy

helpimnotdrowning.net 9fae6eb0 ed5f214b

verified
+1349 -1748
+30 -1
.gitignore
··· 1 - Utatane.psd1 1 + bin/ 2 + obj/ 3 + /packages/ 4 + riderModule.iml 5 + /_ReSharper.Caches/ 6 + 7 + *.DotSettings.user 8 + 9 + # https://github.com/github/gitignore/blob/53fee13f20a05efc93ef4edcad0c62863520e268/Global/JetBrains.gitignore 10 + # Covers JetBrains IDEs: IntelliJ, GoLand, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 11 + # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 12 + 13 + # User-specific stuff 14 + .idea/**/workspace.xml 15 + .idea/**/tasks.xml 16 + .idea/**/usage.statistics.xml 17 + .idea/**/dictionaries 18 + .idea/**/shelf 19 + 20 + # Generated files 21 + .idea/**/contentModel.xml 22 + 23 + # Sensitive or high-churn files 24 + .idea/**/dataSources/ 25 + .idea/**/dataSources.ids 26 + .idea/**/dataSources.local.xml 27 + .idea/**/sqlDataSources.xml 28 + .idea/**/dynamic.xml 29 + .idea/**/uiDesigner.xml 30 + .idea/**/dbnavigator.xml
+15
.idea/.idea.Utatane/.idea/.gitignore
··· 1 + # Default ignored files 2 + /shelf/ 3 + /workspace.xml 4 + # Rider ignored files 5 + /contentModel.xml 6 + /.idea.Utatane.iml 7 + /modules.xml 8 + /projectSettingsUpdater.xml 9 + # Ignored default folder with query files 10 + /queries/ 11 + # Datasource local storage ignored files 12 + /dataSources/ 13 + /dataSources.local.xml 14 + # Editor-based HTTP Client requests 15 + /httpRequests/
+4
.idea/.idea.Utatane/.idea/encodings.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" /> 4 + </project>
+8
.idea/.idea.Utatane/.idea/indexLayout.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="UserContentModel"> 4 + <attachedFolders /> 5 + <explicitIncludes /> 6 + <explicitExcludes /> 7 + </component> 8 + </project>
+6
.idea/.idea.Utatane/.idea/vcs.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="VcsDirectoryMappings"> 4 + <mapping directory="$PROJECT_DIR$" vcs="Git" /> 5 + </component> 6 + </project>
+23
Components/AppTitle.razor
··· 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 + <PageTitle>@( ChildContent == null ? "<somewhere???>" : ChildContent.RenderString() ) :: helpimnotdrowning.net</PageTitle> 19 + 20 + @code { 21 + [Parameter] 22 + public RenderFragment? ChildContent { get; set; } 23 + }
+122
Components/Header.razor
··· 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 + <header class="n-box"> 19 + <div id="breadcrumbs"> 20 + <a href="/" class="clickable" hx-boost="true">/</a> 21 + @{ var pathFragments = Path.Split('/').Skip(1).SkipLast(1).ToList(); } 22 + @for (var i = 0; i < pathFragments.Count; i++) { 23 + var currentCrumbLink = String.Join('/', pathFragments[..i]); 24 + <a href="@currentCrumbLink" class="clickable" hx-boost="true">@pathFragments[i]</a> 25 + if (i == pathFragments.Count) { 26 + <span>/</span> 27 + } 28 + } 29 + </div> 30 + <div id="toolbar"> 31 + <button id="reload-table" 32 + class="clickable" 33 + type="button" 34 + aria-label="Reload table" 35 + _hs=" 36 + -- shortcut: only the active <th> will have aria-sort 37 + on click send nhnd:run to <th[aria-sort]/> 38 + " 39 + > 40 + <span class="[vertical-align:sub]">↻</span> 41 + </button> 42 + @{ var isAtRoot = Path == "/"; } 43 + <a id="go-back" 44 + class="clickable" 45 + href="@(isAtRoot ? null : "..")" 46 + aria-label="Go up 1 directory" 47 + hx-boost="true" 48 + aria-disabled="@isAtRoot" 49 + tabindex="@(isAtRoot ? -1 : 0)" 50 + onClick="@(isAtRoot ? "event.preventDefault();" : null)" 51 + > 52 + <span class="cursor-default! [vertical-align:sub]">../</span> 53 + </a> 54 + 55 + <abbr id="search-label" title="Search">🔎</abbr> 56 + <input id="search" class="border-black" type="search" aria-labelledby="search-label" _hs="@_scriptForIdSearch" /> 57 + 58 + <abbr id="rx-label" title="Search with RegEx">.*</abbr> 59 + <input id="use-rx-for-search" type="checkbox" autocomplete="off" checked aria-labelledby="rx-label" _hs="@_scriptForIdUseRxForSearch" /> 60 + 61 + <span id="file-counter" class="my-auto" _hs="@_scriptForIdFileCounter"></span> 62 + </div> 63 + </header> 64 + 65 + @code { 66 + 67 + [Parameter] public required String Path { get; set; } 68 + 69 + #region _hyperscript strings 70 + // these are just the big ones, smaller ones are littered throughout 71 + private readonly String _scriptForIdSearch = @" 72 + on load 73 + set my value to '' 74 + on input 75 + if (#use-rx-for-search's checked is true) then 76 + make a RegExp from my value called rxSearch 77 + show <tbody > tr/> in #filetable when rxSearch.test(the @data-order of .file-name in it) is true 78 + otherwise 79 + show <tbody > tr/> in #filetable when (the first @data-order of .file-name in it) contains my value.toLowerCase() 80 + end 81 + on keydown from body 82 + make a RegExp from ""^(F|Soft)\\d"" called rx 83 + if not ( 84 + document.activeElement is me or -- dont repeat it! 85 + event.key is """" or -- actually I forgot 86 + event.ctrlKey or event.altKey or event.metaKey or -- dont react on modifier combos 87 + [""Shift"", ""Tab"", ""ArrowUp"", ""ArrowRight"", ""ArrowLeft"", ""ArrowDown"", 88 + ""AltGraph"", ""CapsLock"", ""Fn"", ""FnLock"", ""Hyper"", ""NumLock"", 89 + ""ScrollLock"", ""Super"", ""Symbol"", ""SymbolLock"", ""OS"", ""End"", ""Home"", 90 + ""PageDown"", ""PageUp"", ""Backspace"", ""Clear"", ""Delete"", ""Redo"", 91 + ""Undo"", ""Accept"", ""Again"", ""Attn"", ""Cancel"", ""ContextMenu"", 92 + ""Escape"", ""Find"", ""Pause"", ""Play"", ""Props"", ""Select"", ""ZoomIn"", 93 + ""ZoomOut"", ""Apps"", ""BrightnessDown"", ""BrightnessUp"", ""PrintScreen"", 94 + ""MediaFastForward"", ""MediaPause"", ""MediaPlay"", ""MediaPlayPause"", 95 + ""MediaRecord"", ""MediaRewind"", ""MediaStop"", ""MediaTrackNext"", 96 + ""MediaTrackPrevious""].includes(event.key) or -- special keys to not react on 97 + rx.test(event.key) -- more special keys but these can be regexed 98 + ) 99 + go to the top of the body 100 + focus() on me 101 + 102 + "; 103 + 104 + private readonly String _scriptForIdUseRxForSearch = @" 105 + on load or click -- on load for if this comes checked 106 + if < #use-rx-for-search:checked />'s length 107 + add .font-mono to #search 108 + otherwise 109 + remove .font-mono from #search 110 + end 111 + send input to #search 112 + "; 113 + 114 + private readonly String _scriptForIdFileCounter = @" 115 + on nhnd:update 116 + set files to `$('.file').length 117 + set dirs to `$('.directory').length 118 + set my innerHTML to ```${dirs} $(_icon 'Directories' '📁') `${files} $(_icon 'Files' '📄')`` 119 + "; 120 + #endregion _hyperscript strings 121 + 122 + }
+40
Components/Layout/MainLayout.razor
··· 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 + @inherits LayoutComponentBase 19 + 20 + <!DOCTYPE html> 21 + <html lang="en"> 22 + <head> 23 + <meta charset="utf-8" /> 24 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 25 + 26 + <link rel="stylesheet" type="text/css" href="/.nhnd/style.css" /> 27 + 28 + <script src="/.nhnd/htmx.js"></script> 29 + <meta name="htmx-config" content='{"scrollIntoViewOnBoost":false}' /> 30 + 31 + <meta name="darkreader-lock" /> 32 + 33 + <HeadOutlet /> 34 + @* blank placeholder title, overriden (usually) in @Body *@ 35 + <AppTitle></AppTitle> 36 + </head> 37 + <body> 38 + @Body 39 + </body> 40 + </html>
+52
Components/Pages/Error.razor
··· 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 + @page "/Error" 19 + @using System.Diagnostics 20 + 21 + <PageTitle>Error</PageTitle> 22 + 23 + <h1 class="text-danger">Error.</h1> 24 + <h2 class="text-danger">An error occurred while processing your request.</h2> 25 + 26 + @if (ShowRequestId) { 27 + <p> 28 + <strong>Request ID:</strong> <code>@RequestId</code> 29 + </p> 30 + } 31 + 32 + <h3>Development Mode</h3> 33 + <p> 34 + Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred. 35 + </p> 36 + <p> 37 + <strong>The Development environment shouldn't be enabled for deployed applications.</strong> 38 + It can result in displaying sensitive information from exceptions to end users. 39 + For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong> 40 + and restarting the app. 41 + </p> 42 + 43 + @code{ 44 + [CascadingParameter] private HttpContext? HttpContext { get; set; } 45 + 46 + private string? RequestId { get; set; } 47 + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 48 + 49 + protected override void OnInitialized() => 50 + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; 51 + 52 + }
+164
Components/Pages/Index.razor
··· 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 + @page "/{*Path:nonfile}" 19 + 20 + @* <PageTitle>Index</PageTitle> *@ 21 + <HeadContent> 22 + <script src="/.nhnd/_hyperscript.js"></script> 23 + <script type="text/hyperscript"> 24 + def get_dir(x) 25 + if not x 26 + return "descending" 27 + otherwise 28 + return "ascending" 29 + end 30 + end 31 + 32 + behavior sort_table(property) 33 + on click 34 + -- wait 0.1s 35 + remove @@aria-sort from <th/> 36 + 37 + set target to the first <#prop-${property}/> 38 + set dir_target to the first <input[name="sort-asc"]/> 39 + 40 + if (target's checked is true) 41 + set dir_target's checked to (not dir_target's checked) 42 + otherwise 43 + set <input[name=sort-by]/>'s checked to false 44 + set dir_target's checked to true 45 + set target's checked to true 46 + end 47 + 48 + set my @@aria-sort to get_dir((dir_target's checked)) 49 + send nhnd:run to me 50 + end 51 + end 52 + </script> 53 + </HeadContent> 54 + 55 + <Header Path="@Path" /> 56 + 57 + @* redirect notice here... *@ 58 + 59 + <div class="n-box flex flex-row"> 60 + <div id="main"> 61 + <form id="sorting-on" class="hidden"> 62 + <input autocomplete="off" type="radio" name="sort-by" value="name" id="prop-name" checked /> 63 + <input autocomplete="off" type="radio" name="sort-by" value="size" id="prop-size" /> 64 + <input autocomplete="off" type="radio" name="sort-by" value="time" id="prop-time" /> 65 + 66 + <input autocomplete="off" type="checkbox" name="sort-asc" checked /> 67 + </form> 68 + 69 + <table id="filetable" _="on htmx:afterSettle send nhnd:update to #file-counter"> 70 + <thead> 71 + <tr hx-include="#sorting-on input"> 72 + @{ String InstallSort(String x) => $"install sort_table(property: '{x}') end"; } 73 + <th id="filetable-head-dl" class="pointer-events-none" tabindex="0"> 74 + <span class="visually-hidden">Download link</span> 75 + </th> 76 + 77 + <th id="filetable-head-icon" class="pointer-events-none" tabindex="0"> 78 + <span class="visually-hidden">File type icon</span> 79 + </th> 80 + 81 + <th id="filetable-head-name" 82 + hx-trigger="load from:document, nhnd:run" 83 + hx-get="/api/files?path=@Path" 84 + hx-target="#filetable tbody" 85 + hx-swap="innerHtml" 86 + hx-indicator="#filetable-body, #name-spinner, #filetable-head-name" 87 + aria-sort="ascending" 88 + _="@InstallSort("name")" 89 + > 90 + <div> 91 + <span class="header-label"> 92 + Name 93 + <span id="name-spinner" class="spinner" /> 94 + </span> 95 + </div> 96 + </th> 97 + 98 + <th id="filetable-head-size" 99 + hx-trigger="nhnd:run" 100 + hx-get="/api/files?path=@Path" 101 + hx-target="#filetable tbody" 102 + hx-swap="innerHtml" 103 + hx-indicator="#filetable-body, #name-spinner, #filetable-head-size" 104 + _="@InstallSort("size")" 105 + > 106 + <div> 107 + <span class="header-label"> 108 + Size 109 + <span id="size-spinner" class="spinner" /> 110 + </span> 111 + </div> 112 + </th> 113 + 114 + <th id="filetable-head-time" 115 + hx-trigger="nhnd:run" 116 + hx-get="/api/files?path=@Path" 117 + hx-target="#filetable tbody" 118 + hx-swap="innerHtml" 119 + hx-indicator="#filetable-body, #name-spinner, #filetable-head-time" 120 + _="@InstallSort("time")" 121 + > 122 + <div> 123 + <span class="header-label"> 124 + Time 125 + <span id="time-spinner" class="spinner" /> 126 + </span> 127 + </div> 128 + </th> 129 + </tr> 130 + </thead> 131 + <tbody id="filetable-body" class="htmx-indicator"> 132 + @* pregen rows to prevent massive layout shift *@ 133 + @foreach (FileSystemInfo fsi in _joinedPath.EnumerateFileSystemInfos()) { 134 + <tr> 135 + <td /> 136 + <td /> 137 + <td>@( "\u00A0" )</td> 138 + <td /> 139 + <td /> 140 + </tr> 141 + } 142 + </tbody> 143 + </table> 144 + </div> 145 + </div> 146 + 147 + 148 + @code { 149 + [Parameter] 150 + public required String Path { 151 + get; 152 + set => field = $"/{value}"; 153 + } 154 + 155 + private DirectoryInfo _joinedPath; 156 + 157 + protected override Task OnInitializedAsync() { 158 + // we checked in middleware dw 159 + _joinedPath = Utils.CheckJoinedPathIsBased(Path).Value; 160 + 161 + return base.OnInitializedAsync(); 162 + } 163 + 164 + }
+22
Components/Pages/NotFound.razor
··· 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 + @page "/not-found" 19 + @layout MainLayout 20 + 21 + <h3>Not Found</h3> 22 + <p>Sorry, the content you are looking for does not exist.</p>
+159
Components/Pages/api/Files.razor
··· 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 + @page "/api/files" 19 + @layout EmptyLayout 20 + 21 + @foreach (var row in _rows) { 22 + <tr class="fsobject @row.StringType"> 23 + <td> 24 + @if (row.IsFile && !row.IsBad) { 25 + <a 26 + download 27 + class="file-dl clickable" 28 + href="@row.Href" 29 + aria-label="Download file" 30 + >⭳</a> 31 + } 32 + </td> 33 + <td>@row.Icon</td> 34 + <td class="file-name" data-order="@row.Name"> 35 + @if (row.IsBad) { 36 + <s class="text-stone-500"> 37 + @(row.Name + row.Trail) 38 + </s> 39 + } else { 40 + <a href="@(row.Href + row.Trail)" hx-boost="@(!row.IsFile)"> 41 + @row.Name 42 + </a> 43 + @if (!row.IsFile) { 44 + <span class="dir-slash">/</span> 45 + } 46 + } 47 + </td> 48 + <td class="file-size" data-order="@row.Size"> 49 + @if (row.IsFile) { 50 + @if (row.IsBad) { 51 + <s>@Utils.FormatFileSize(row.Size)</s> 52 + } else { 53 + @Utils.FormatFileSize(row.Size); 54 + } 55 + } 56 + </td> 57 + <td class="file-date" data-order="@row.TimeUnix"> 58 + @if (row.IsBad) { 59 + <s><time>@row.TimeFmt</time></s> 60 + } else { 61 + <time>@row.TimeFmt</time> 62 + } 63 + </td> 64 + </tr> 65 + } 66 + 67 + @code { 68 + 69 + [SupplyParameterFromQuery(Name = "path")] 70 + public required String? Path { get; set; } 71 + 72 + [SupplyParameterFromQuery(Name = "sort-by")] 73 + public required String? SortByColumn { get; set; } 74 + 75 + [SupplyParameterFromQuery(Name = "sort-asc")] 76 + public required String SortAsc { get; set; } 77 + 78 + private DirectoryInfo _realPath; 79 + private SortByColumns _sortByColumn; 80 + private SortByDirection _sortByDirection; 81 + private IEnumerable<FileRow> _rows; 82 + 83 + protected override Task OnInitializedAsync() { 84 + // _path = Path == "/" ? "/" : $"/{Path}"; 85 + // prechecked in middleware! 86 + _realPath = Utils.CheckJoinedPathIsBased(Path ?? "/").Value; 87 + _sortByColumn = SortByColumn switch { 88 + "name" => SortByColumns.Name, 89 + "size" => SortByColumns.Size, 90 + "time" => SortByColumns.Time, 91 + _ => SortByColumns.Name 92 + }; 93 + _sortByDirection = SortAsc switch { 94 + "on" => SortByDirection.Ascending, 95 + _ => SortByDirection.Descending 96 + 97 + }; 98 + _rows = _realPath.EnumerateFileSystemInfos() 99 + .Select(fsi => new FileRow(fsi!, Path ?? "/")) 100 + .OrderByDescending(x => !x.IsFile) 101 + .ThenBy(x => x.Name); 102 + 103 + Console.WriteLine($"Path <{Path}> | Col <{SortByColumn}> | Dir <{SortAsc}>"); 104 + Console.WriteLine($"RealPath <{_realPath}> | Col <{_sortByColumn}> | Dir <{_sortByDirection}>"); 105 + 106 + return base.OnInitializedAsync(); 107 + } 108 + 109 + public enum SortByColumns { 110 + Name, 111 + Size, 112 + Time, 113 + } 114 + 115 + public enum SortByDirection { 116 + Ascending, 117 + Descending, 118 + } 119 + 120 + public class FileRow { 121 + public FileSystemInfo Fsi { get; } 122 + public bool IsFile { get; } 123 + public String StringType { get; } 124 + public bool IsBad { get; } 125 + public MarkupString Icon { get; } 126 + public String Name { get; } 127 + public long Size { get; } 128 + public String TimeFmt { get; } 129 + public String TimeUnix { get; } 130 + public String? Href { get; } 131 + public char? Trail { get; } 132 + 133 + public FileRow(FileSystemInfo baseFsi, String currentPath) { 134 + bool isLink = baseFsi.Attributes.HasFlag(FileAttributes.ReparsePoint); 135 + 136 + Fsi = isLink ? baseFsi.ReadLink()! : baseFsi; 137 + 138 + IsFile = Fsi is FileInfo; 139 + StringType = IsFile ? "file" : "directory"; 140 + IsBad = !Fsi.IsReadable(); 141 + Icon = IsBad 142 + ? Utils.AbbrIcon($"Server does not have permission to read this {StringType}", "⚠️") 143 + : Utils.GetIconForFileType(Fsi); 144 + Name = baseFsi.Name; // specifically want whatever its called in the directory we're looking at 145 + Size = IsFile ? ((FileInfo)Fsi).Length : -1; 146 + TimeFmt = Fsi.LastWriteTime.ToUniversalTime().ToString("yyyy-MM-dd HH:mm"); 147 + TimeUnix = $"{Fsi.LastWriteTime.ToUniversalTime().Subtract(DateTime.UnixEpoch).TotalSeconds:N0}"; 148 + Href = IsBad 149 + ? null 150 + : System.IO.Path.Join(currentPath, Name); 151 + Trail = IsFile ? null : '/'; 152 + 153 + if (isLink) { 154 + Console.WriteLine($"name {Name} isfile {IsFile} size {Size}"); 155 + } 156 + } 157 + 158 + } 159 + }
+22
Components/Routes.razor
··· 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 + <Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)"> 19 + <Found Context="routeData"> 20 + <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/> 21 + </Found> 22 + </Router>
+28
Components/_Imports.razor
··· 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 + @using System.Net.Http 19 + @using System.Net.Http.Json 20 + @using Microsoft.AspNetCore.Components.Forms 21 + @using Microsoft.AspNetCore.Components.Routing 22 + @using Microsoft.AspNetCore.Components.Web 23 + @using static Microsoft.AspNetCore.Components.Web.RenderMode 24 + @using Microsoft.AspNetCore.Components.Web.Virtualization 25 + @using Microsoft.JSInterop 26 + @using Utatane 27 + @using Utatane.Components 28 + @using Utatane.Components.Layout
+14 -15
Dockerfile
··· 1 - FROM helpimnotdrowning/pode:2.12.1 2 - SHELL ["pwsh", "-c"] 1 + FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base 2 + ENV ASPNETCORE_HTTP_PORTS=8081 3 + WORKDIR /app 4 + EXPOSE 8081 3 5 4 - RUN apt-get update 5 - RUN apt-get install git -y 6 + FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build 7 + WORKDIR /src/Utatane 8 + COPY Utatane.csproj . 9 + RUN dotnet restore Utatane.csproj 10 + COPY . . 11 + RUN dotnet publish -o /app/build 6 12 7 - RUN git clone https://github.com/helpimnotdrowning/Mizumiya --branch v0.2.0 /tmp/Mizumiya_repo 8 - RUN mkdir -p /usr/local/share/powershell/Modules/Mizumiya 9 - RUN cp -r /tmp/Mizumiya_repo/Mizumiya/* /usr/local/share/powershell/Modules/Mizumiya 10 - 11 - RUN Set-PSRepository -Name PSGallery -InstallationPolicy Trusted 12 - RUN Install-Module -Name PSParseHTML -RequiredVersion 2.0.2 13 - 14 - COPY . /app/Utatane/ 15 - 16 - EXPOSE 8081 13 + FROM base AS final 14 + WORKDIR /app 15 + COPY --from=build /app/build /app/Utatane 17 16 WORKDIR /app/Utatane 18 - CMD [ "pwsh", "-c", "./Server.ps1" ] 17 + CMD [ "./Utatane" ]
+122
Program.cs
··· 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 + using System.IO.Compression; 19 + using System.Text; 20 + using FluentResults; 21 + using Microsoft.AspNetCore.Http.Features; 22 + using Utatane.Components; 23 + using Microsoft.AspNetCore.ResponseCompression; 24 + using Microsoft.AspNetCore.Rewrite; 25 + using Microsoft.Extensions.FileProviders; 26 + using Utatane; 27 + 28 + var builder = WebApplication.CreateBuilder(new WebApplicationOptions() { 29 + Args = args, 30 + WebRootPath = "public" 31 + }); 32 + 33 + // Add services to the container. 34 + // Add services to the container. 35 + builder.Services.AddRazorComponents(); 36 + builder.Services.AddResponseCompression(options => { 37 + options.EnableForHttps = true; 38 + options.Providers.Add<BrotliCompressionProvider>(); 39 + options.Providers.Add<GzipCompressionProvider>(); 40 + options.MimeTypes = ResponseCompressionDefaults.MimeTypes; 41 + } ); 42 + builder.Services.Configure<BrotliCompressionProviderOptions>(options => { 43 + options.Level = CompressionLevel.SmallestSize; 44 + }); 45 + 46 + builder.Services.Configure<GzipCompressionProviderOptions>(options => { 47 + options.Level = CompressionLevel.SmallestSize; 48 + }); 49 + 50 + var app = builder.Build(); 51 + 52 + // Configure the HTTP request pipeline. 53 + if (!app.Environment.IsDevelopment()) { 54 + app.UseExceptionHandler("/Error", createScopeForErrors: true); 55 + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 56 + app.UseHsts(); 57 + } 58 + 59 + app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); 60 + app.UseHttpsRedirection(); 61 + 62 + app.UseAntiforgery(); 63 + app.UseResponseCompression(); 64 + 65 + app.Use(async (context, next) => { 66 + // context.Response.Body is a direct line to the client, so 67 + // swap it out for our own in-memory stream for now 68 + Stream responseStream = context.Response.Body; 69 + using var memoryStream = new MemoryStream(); 70 + context.Response.Body = memoryStream; 71 + 72 + // let downstream render the response & write to our stream 73 + await next(context); 74 + 75 + if ( 76 + context.Response.ContentType?.StartsWith("text/html") != true 77 + /* || TODO: don't compress when serving HTML files found by Utatane!! */ ) { 78 + // oops my bad gangalang 79 + // ok now put it back 80 + memoryStream.Position = 0; 81 + await memoryStream.CopyToAsync(responseStream); 82 + context.Response.Body = responseStream; 83 + 84 + return; 85 + } 86 + 87 + memoryStream.Position = 0; 88 + String html = await new StreamReader(memoryStream).ReadToEndAsync(); 89 + String minified = Utils.OptimizeHtml(html); 90 + 91 + context.Response.ContentLength = Encoding.UTF8.GetByteCount(minified); 92 + await responseStream.WriteAsync(Encoding.UTF8.GetBytes(minified)); 93 + context.Response.Body = responseStream; 94 + }); 95 + 96 + // check paths exist 97 + app.Use(async (context, next) => { 98 + if ( 99 + context.Request.Path.StartsWithSegments("/api/files") 100 + || context.Request.Path.StartsWithSegments("/.nhnd") 101 + ) { 102 + await next(context); 103 + return; 104 + } 105 + 106 + var pathCheck = Utils.CheckJoinedPathIsBased(context.Request.Path); 107 + if (pathCheck.IsFailed) { 108 + context.Response.StatusCode = StatusCodes.Status404NotFound; 109 + return; 110 + } 111 + 112 + await next(context); 113 + }); 114 + 115 + app.UseRewriter(new RewriteOptions().AddRedirect(@"^favicon\.ico$", "/.nhnd/favicon.ico")); 116 + 117 + app.UseStaticFiles(new StaticFileOptions { 118 + RequestPath = "/.nhnd" 119 + }); 120 + app.MapRazorComponents<App>(); 121 + 122 + app.Run();
+29
Properties/launchSettings.json
··· 1 + { 2 + "$schema": "https://json.schemastore.org/launchsettings.json", 3 + "profiles": { 4 + "http": { 5 + "commandName": "Project", 6 + "dotnetRunMessages": true, 7 + "launchBrowser": true, 8 + "applicationUrl": "http://localhost:5180", 9 + "environmentVariables": { 10 + "ASPNETCORE_ENVIRONMENT": "Development", 11 + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Tailwind.Hosting", 12 + "DEBUG": "tailwindcss:*", 13 + "nhnd_Utatane_ROOT": "/home/helpimnotdrowning/ww/" 14 + } 15 + }, 16 + "https": { 17 + "commandName": "Project", 18 + "dotnetRunMessages": true, 19 + "launchBrowser": true, 20 + "applicationUrl": "https://localhost:7299;http://localhost:5180", 21 + "environmentVariables": { 22 + "ASPNETCORE_ENVIRONMENT": "Development", 23 + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Tailwind.Hosting", 24 + "DEBUG": "tailwindcss:*", 25 + "nhnd_Utatane_ROOT": "/" 26 + } 27 + } 28 + } 29 + }
-267
Routes.ps1
··· 1 - <# 2 - This file is part of Utatane. 3 - 4 - Utatane is free software: you can redistribute it and/or modify it under the 5 - 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 - Import-Module Mizumiya 19 - Import-Module PSParseHTML -Function Optimize-HTML 20 - 21 - $Root = (Get-PodeConfig).Utatane.Root 22 - 23 - if (-not (Get-Command tailwindcss -ErrorAction SilentlyContinue)) { 24 - _warn "The tailwindcss binary is missing. Source styles will not be synced with the public style.css." 25 - _warn "This is only needed during development, don't worry about it in production" 26 - } else { 27 - Add-PodeFileWatcher -Path $PSScriptRoot -Exclude "$PSScriptRoot/public" -ScriptBlock { 28 - tailwindcss --input $PSScriptRoot/tailwind/style.tw.css --output $PSScriptRoot/public/style.css 29 - } 30 - } 31 - 32 - _info "Serving root directory $Root" 33 - 34 - # Use-PodeScript -Path functions.ps1 # https://github.com/Badgerati/Pode/issues/1582 35 - Use-PodeScript -Path $PSScriptRoot/functions.ps1 36 - 37 - New-PodeLoggingMethod -Custom -ScriptBlock { 38 - param ($Item) 39 - 40 - $Date = $Item.UtcDate | Get-Date -Format s 41 - $Query = $Item.Request.Query -eq '-' ? '' : '?' + $Item.Request.Query 42 - 43 - $PrevCol = $PSStyle.Reset + $PSStyle.Foreground.BrightBlack 44 - switch ($Item.Response.StatusCode) { 45 - {$_ -ge 400} { $Col = $PSStyle.Foreground.BrightRed } 46 - {$_ -ge 500} { $Col = $PSStyle.Foreground.White + $PSStyle.Background.Red } 47 - default { $Col = '' } 48 - } 49 - 50 - $RequestLine = "$($Item.Host) $($Item.Request.Method) $($PSStyle.Foreground.White)$($Item.Request.Resource)$Query$PrevCol $($Item.Request.Protocol) by ""$($Item.Request.Agent)""" 51 - $ResponseLine = "$Col$($Item.Response.StatusCode) $($Item.Response.StatusDescription)$PrevCol $(_format_size $Item.Response.Size)" 52 - 53 - _request "$Date | $RequestLine >>> $ResponseLine" 54 - } | Enable-PodeRequestLogging -Raw 55 - 56 - New-PodeLoggingMethod -Custom -ScriptBlock { 57 - param ($Item) 58 - 59 - _warn "$($Item.Date | Get-Date -Format s) | $($Item.Level) ($($Item.Server):$($Item.ThreadId)) | $($Item.Category): $($Item.Message)" 60 - $Item.StackTrace -split "`n" | % { _warn $_ } 61 - } | Enable-PodeErrorLogging -Raw -Levels Error, Warning, Informational 62 - 63 - Add-PodeEndpoint -Address * -Port 8080 -Protocol HTTP 64 - 65 - Set-PodeViewEngine -Type Mizumiya -Extension ps1 -ScriptBlock { 66 - param ($Path, $Data) 67 - 68 - . ./functions.ps1 69 - 70 - <# 71 - FOR JSON: Output any object and ConvertTo-Json will handle it 72 - FOR XMLs and HTML: Your script MUST output a well-formed string in your 73 - target format; PowerShell's ConvertTo-Xml is not built for this 74 - purpose 75 - #> 76 - switch -Wildcard ($Path) { 77 - '*.html.ps1' { 78 - Set-PodeHeader -Name Content-Type -Value 'text/html; charset=utf-8' 79 - return ([String] (. $Path -Data $Data)) | Optimize-HTML 80 - } 81 - 82 - '*.json.ps1' { 83 - Set-PodeHeader -Name Content-Type -Value 'application/json; charset=utf-8' 84 - return [PSCustomObject]((. $Path -Data $Data) ?? @{}) | ConvertTo-Json -Compress -Depth 8 85 - } 86 - 87 - '*.xml.ps1' { 88 - Set-PodeHeader -Name Content-Type -Value 'application/xml; charset=utf-8' 89 - return ([String] (. $Path -Data $Data)) 90 - } 91 - 92 - '*.xhtml.ps1' { 93 - Set-PodeHeader -Name Content-Type -Value 'application/xhtml+xml; charset=utf-8' 94 - return ([String] (. $Path -Data $Data)) 95 - } 96 - 97 - '*.atom.ps1' { 98 - Set-PodeHeader -Name Content-Type -Value 'application/atom+xml; charset=utf-8' 99 - return ([String] (. $Path -Data $Data)) 100 - } 101 - } 102 - } 103 - 104 - $Middleware_DirsHaveTrailingSlash = { 105 - $Root = $using:Root 106 - $Path = $WebEvent.Path 107 - $JoinedPath = Get-Item -Lit (Join-Path $Root $Path) 108 - 109 - if (-not (_is_file $JoinedPath) -and $Path -ne '/' -and $Path -notmatch '/$') { 110 - Move-PodeResponseUrl -Url ("$Path/") 111 - return $false 112 - } 113 - 114 - return $true 115 - } 116 - 117 - Add-PodeMiddleware -Name CheckRootIsAvailable -ScriptBlock { 118 - $Root = $using:Root 119 - 120 - if (-not (Test-Path -LiteralPath $Root -PathType Container)) { 121 - _log "Root $Root not available!" -Type Warning 122 - Set-PodeResponseStatus -Code 503 -Description 'The root directory is not available.' 123 - return $false 124 - } 125 - 126 - return $true 127 - } 128 - 129 - Add-PodeMiddleware -Name CreateAcceptInWebEvent -ScriptBlock { 130 - $WebEvent.RequestAccept = ((Get-PodeHeader 'Accept') ?? 'text/html' | ConvertFrom-AcceptHeader) 131 - 132 - return $true 133 - } 134 - 135 - Add-PodeMiddleware -Name __pode_mw_static_content__ -ScriptBlock { 136 - if ($WebEvent.Path -eq '/favicon.ico') { 137 - $pubRoute = Find-PodePublicRoute -Path '/favicon.ico' 138 - 139 - if ($null -eq $pubRoute) { 140 - Set-PodeResponseStatus -Code 404 141 - return $false 142 - } 143 - 144 - $cachable = Test-PodeRouteValidForCaching -Path '/favicon.ico' 145 - 146 - Write-PodeFileResponse -FileInfo $pubRoute.FileInfo -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$cachable 147 - return $false 148 - } 149 - 150 - if (-not ($WebEvent.Path -like '/.nhnd/*') -or $WebEvent.Path.Length -lt 6) { 151 - return $true 152 - } 153 - 154 - if ($WebEvent.Path -in ('/.nhnd', '/.nhnd/')) { 155 - Set-PodeResponseStatus -Code 404 156 - return $false 157 - } 158 - 159 - $_path = $WebEvent.Path.SubString(6) # strip /.nhnd 160 - $pubRoute = Find-PodePublicRoute -Path $_path 161 - 162 - if ($null -eq $pubRoute) { 163 - # using our own directory so we can just blanket deny 164 - Set-PodeResponseStatus -Code 404 165 - return $false 166 - } 167 - 168 - # check current state of caching 169 - $cachable = Test-PodeRouteValidForCaching -Path $_path 170 - 171 - # write the file to the response 172 - Write-PodeFileResponse -FileInfo $pubRoute.FileInfo -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$cachable 173 - return $false 174 - } 175 - 176 - Enable-PodeSessionMiddleware -Duration 120 -Extend 177 - 178 - Add-PodeRouteGroup -Path /api -Routes { 179 - Add-PodeRoute -Path /files -Method GET -ScriptBlock { 180 - $Root = $using:Root 181 - $Path = $WebEvent.Query.Path 182 - $JoinedPath = Join-Path $Root $Path 183 - 184 - foreach ($Redirect in (Get-PodeConfig).Utatane.RedirectMap.GetEnumerator()) { 185 - if (-not ($Path -eq $Redirect.Key -or ($Path + '/') -eq $Redirect.Key)) { 186 - continue 187 - } 188 - 189 - $Url = ("/api/files?path=" + (_encode $Redirect.Value)) 190 - 191 - # api doesn't get a message since they probably aren't keeping the 192 - # cookie needed for session tracking 193 - Move-PodeResponseUrl -Url $Url 194 - return 195 - } 196 - 197 - if (-not (_check_path $Root $JoinedPath)) { 198 - _warn "/api/files: query for $JoinedPath ($Path) refused!" 199 - Set-PodeResponseStatus -Code 404 200 - return; 201 - } 202 - 203 - $Data = @{ Root=$Root; Path=$Path; JoinedPath=$JoinedPath} 204 - _render_view -Page api/files -Data $Data 205 - } 206 - 207 - Add-PodeRoute -Path /* -Method GET -ScriptBlock { 208 - Set-PodeResponseStatus -Code 404 209 - } 210 - } 211 - 212 - Add-PodeRoute -Method GET -Path /about -ScriptBlock { 213 - _render_view about 214 - } 215 - 216 - Add-PodeRoute -Method GET -Path /* -Middleware $Middleware_DirsHaveTrailingSlash -ScriptBlock { 217 - $Root = $using:Root 218 - $Path = $WebEvent.Path ?? '/' 219 - $JoinedPath = Get-Item -Lit (Join-Path $Root $Path) 220 - 221 - foreach ($Redirect in (Get-PodeConfig).Utatane.RedirectMap.GetEnumerator()) { 222 - if ($Path -ne $Redirect.Key) { 223 - continue; 224 - } 225 - 226 - Add-PodeFlashMessage -name redirect-notice -Message @{ 227 - From = $Path 228 - To = $Redirect.Value 229 - } 230 - 231 - Move-PodeResponseUrl -Url $Redirect.Value 232 - return; 233 - } 234 - 235 - if ($null -eq $JoinedPath) { 236 - _warn "1failed: '$(Join-Path $Root $Path)' for query $($WebEvent.Query|convertto-json -compress)" 237 - Set-PodeResponseStatus -Code 404 238 - return 239 - } 240 - 241 - # No path traversal, no blacklist! 242 - if (-not (_check_path $Root $JoinedPath)) { 243 - _warn "denied $Path ($JoinedPath)" 244 - Set-PodeResponseStatus -Code 404 245 - return 246 - } 247 - 248 - $IsFile = _is_file $JoinedPath 249 - 250 - if ($IsFile) { 251 - Set-PodeResponseAttachment -path ([WildcardPattern]::Escape($JoinedPath)) 252 - return 253 - } else { 254 - # also don't let the server serve itself 255 - if ("$JoinedPath/".StartsWith($using:PSScriptRoot)) { 256 - _warn 'failed: refusing to serve the server root' 257 - Set-PodeResponseStatus -Code 404 258 - } 259 - 260 - _render_view -Page index -Data @{ Root=$Root ; Path=$Path } 261 - return 262 - } 263 - 264 - _warn "failed: '$Root$Rath' for query $($WebEvent.Query|convertto-json -compress)" 265 - Set-PodeResponseStatus -Code 404 266 - return 267 - }
-47
Server.ps1
··· 1 - #!/bin/pwsh 2 - <# 3 - This file is part of Utatane. 4 - 5 - Utatane is free software: you can redistribute it and/or modify it under the 6 - terms of the GNU Affero General Public License as published by the Free 7 - Software Foundation, either version 3 of the License, or (at your option) 8 - any later version. 9 - 10 - Utatane is distributed in the hope that it will be useful, but WITHOUT ANY 11 - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 - FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 13 - more details. 14 - 15 - You should have received a copy of the GNU Affero General Public License 16 - along with Utatane. If not, see <http://www.gnu.org/licenses/>. 17 - #> 18 - 19 - Import-Module Pode 20 - Import-Module Mizumiya 21 - 22 - # its called we do a little bit of trolling 23 - . (Get-Module Pode) { 24 - # TLDR: purposefully and painfully trample the safety of psd1 files 25 - # apparently, you can execute code in a module's scope, which allows us to 26 - # hot-patch away the little obstacle of not being able to use "dynamic 27 - # expressions" in the psd1 config file. the whole point of psd1 files is 28 - # that they are a *safe* way to load data in powershell format, but I don't 29 - # really care about that. personally, this feels akin to beating the runtime 30 - # over the head with a wrench 31 - # further reading: https://seeminglyscience.github.io/powershell/2017/09/30/invocation-operators-states-and-scopes 32 - function Import-PowerShellDataFile { 33 - [CmdletBinding()] 34 - param ( 35 - [String] $Path 36 - ) 37 - 38 - return (Invoke-Expression -Command (Get-Content -LiteralPath $Path -Raw) -ErrorAction Stop) 39 - } 40 - } 41 - 42 - . ./functions.ps1 43 - 44 - Start-PodeServer { 45 - # allow dynamic reload of routes without a full server restart (do Ctrl+R !) 46 - . ./Routes.ps1 47 - }
+38
Utatane.csproj
··· 1 + <Project Sdk="Microsoft.NET.Sdk.Web"> 2 + 3 + <PropertyGroup> 4 + <TargetFramework>net10.0</TargetFramework> 5 + <Nullable>enable</Nullable> 6 + <ImplicitUsings>enable</ImplicitUsings> 7 + <BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException> 8 + <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> 9 + 10 + <TailwindVersion>v4.1.18</TailwindVersion> 11 + <TailwindWatch>true</TailwindWatch> 12 + <TailwindInputCssFile>Tailwind/style.tw.css</TailwindInputCssFile> 13 + <TailwindOutputCssFile>public/style.css</TailwindOutputCssFile> 14 + <TailwindMinifyOnPublish>false</TailwindMinifyOnPublish> 15 + </PropertyGroup> 16 + 17 + <ItemGroup> 18 + <Content Include="public/**/*"> 19 + <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> 20 + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> 21 + </Content> 22 + </ItemGroup> 23 + 24 + <Target Name="UsePublicNotWwwroot" AfterTargets="Publish"> 25 + <RemoveDir Directories="$(PublishDir)/wwwroot" /> 26 + </Target> 27 + 28 + <ItemGroup> 29 + <PackageReference Include="FluentResults" Version="4.0.0" /> 30 + <PackageReference Include="NUglify" Version="1.21.17" /> 31 + <PackageReference Include="Tailwind.Hosting" Version="1.2.4" /> 32 + <PackageReference Include="Tailwind.Hosting.Build" Version="1.2.4"> 33 + <PrivateAssets>all</PrivateAssets> 34 + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> 35 + </PackageReference> 36 + </ItemGroup> 37 + 38 + </Project>
-9
Utatane.example.psd1
··· 1 - @{ 2 - Root = '/media/share' 3 - DoNotServe = @( 4 - # '*cookies.txt' 5 - ) 6 - RedirectMap = @{ 7 - # '/old_path/' = '/new/path/' 8 - } 9 - }
+16
Utatane.sln
··· 1 +  2 + Microsoft Visual Studio Solution File, Format Version 12.00 3 + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utatane", "Utatane.csproj", "{1FA88E11-0FEA-4449-8299-0302FAC63301}" 4 + EndProject 5 + Global 6 + GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 + Debug|Any CPU = Debug|Any CPU 8 + Release|Any CPU = Release|Any CPU 9 + EndGlobalSection 10 + GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 + {1FA88E11-0FEA-4449-8299-0302FAC63301}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 + {1FA88E11-0FEA-4449-8299-0302FAC63301}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 + {1FA88E11-0FEA-4449-8299-0302FAC63301}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 + {1FA88E11-0FEA-4449-8299-0302FAC63301}.Release|Any CPU.Build.0 = Release|Any CPU 15 + EndGlobalSection 16 + EndGlobal
+371
Utils.cs
··· 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 + 20 + using System.Runtime.InteropServices; 21 + using System.Text; 22 + using FluentResults; 23 + using Microsoft.AspNetCore.Components; 24 + using Microsoft.AspNetCore.Components.Rendering; 25 + using Microsoft.AspNetCore.Components.RenderTree; 26 + using Microsoft.AspNetCore.Mvc.Rendering; 27 + using NUglify; 28 + using NUglify.Html; 29 + 30 + namespace Utatane; 31 + 32 + public static class Utils { 33 + public static String Root = Environment.GetEnvironmentVariable("nhnd_Utatane_ROOT") ?? throw new NullReferenceException("env nhnd_Utatane_ROOT not set!"); 34 + 35 + public static List<String> DoNotServe = ["*cookies.txt"]; 36 + 37 + private static readonly HtmlSettings HtmlSettings = new HtmlSettings() { 38 + RemoveComments = false, 39 + RemoveOptionalTags = false, 40 + RemoveInvalidClosingTags = false, 41 + RemoveEmptyAttributes = false, 42 + RemoveScriptStyleTypeAttribute = false, 43 + ShortBooleanAttribute = false, 44 + IsFragmentOnly = true, 45 + MinifyJs = false, 46 + MinifyJsAttributes = false, 47 + MinifyCss = false, 48 + MinifyCssAttributes = false, 49 + }; 50 + 51 + public static String OptimizeHtml(String html) { 52 + return Uglify.Html(html, HtmlSettings).Code ?? String.Empty; 53 + } 54 + 55 + public static Result<DirectoryInfo> CheckJoinedPathIsBased(String path) { 56 + var resolved = new DirectoryInfo(Path.Join(Root, path)); 57 + 58 + if ( 59 + !resolved.Exists 60 + || !resolved.FullName.StartsWith(Root) 61 + || DoNotServe.Contains(resolved.Name) 62 + ) { 63 + return Result.Fail("Bad path"); 64 + } 65 + 66 + return Result.Ok(resolved); 67 + } 68 + 69 + public static String FormatFileSize(Int64 size) { 70 + const Int64 terabyte = 1099511627776; // 2**40 71 + const Int64 gigabyte = 1073741824; // 2**30 72 + const Int64 megabyte = 1048576; // 2**20 73 + const Int64 kilobyte = 1024; // 2**10 74 + 75 + double sizeD = size; 76 + 77 + return size switch { 78 + > terabyte => $"{(sizeD / terabyte):F2} TiB ", 79 + > gigabyte => $"{(sizeD / gigabyte):F2} GiB", 80 + > megabyte => $"{(sizeD / megabyte):F2} MiB", 81 + > kilobyte => $"{(sizeD / kilobyte):F2} KiB", 82 + _ => $"{size} B" 83 + }; 84 + } 85 + 86 + public static MarkupString AbbrIcon(String text, String symbol) { 87 + RenderTreeBuilder renderer = new RenderTreeBuilder(); 88 + 89 + var abbr = new TagBuilder("abbr"); 90 + abbr.Attributes.Add("title", text); 91 + abbr.InnerHtml.Append(symbol); 92 + 93 + using var writer = new System.IO.StringWriter(); 94 + abbr.WriteTo(writer, System.Text.Encodings.Web.HtmlEncoder.Default); 95 + 96 + return (MarkupString)writer.ToString(); 97 + } 98 + 99 + private static readonly Dictionary<String, MarkupString> ExtToAbbr = new Dictionary<String, MarkupString> { 100 + #region Extension <--> Icon mappings 101 + { ".avc", AbbrIcon("Video file", "🎞️") }, 102 + { ".flv", AbbrIcon("Video file", "🎞️") }, 103 + { ".mts", AbbrIcon("Video file", "🎞️") }, 104 + { ".m2ts", AbbrIcon("Video file", "🎞️") }, 105 + { ".m4v", AbbrIcon("Video file", "🎞️") }, 106 + { ".mkv", AbbrIcon("Video file", "🎞️") }, 107 + { ".mov", AbbrIcon("Video file", "🎞️") }, 108 + { ".mp4", AbbrIcon("Video file", "🎞️") }, 109 + { ".ts", AbbrIcon("Video file", "🎞️") }, 110 + { ".webm", AbbrIcon("Video file", "🎞️") }, 111 + { ".wmv", AbbrIcon("Video file", "🎞️") }, 112 + 113 + { ".aac", AbbrIcon("Audio file", "🔊") }, 114 + { ".alac", AbbrIcon("Audio file", "🔊") }, 115 + { ".flac", AbbrIcon("Audio file", "🔊") }, 116 + { ".m4a", AbbrIcon("Audio file", "🔊") }, 117 + { ".mp3", AbbrIcon("Audio file", "🔊") }, 118 + { ".opus", AbbrIcon("Audio file", "🔊") }, 119 + { ".wav", AbbrIcon("Audio file", "🔊") }, 120 + { ".ogg", AbbrIcon("Audio file", "🔊") }, 121 + { ".mus", AbbrIcon("Audio file", "🔊") }, 122 + 123 + { ".avif", AbbrIcon("Image file", "🖼️") }, 124 + { ".bmp", AbbrIcon("Image file", "🖼️") }, 125 + { ".gif", AbbrIcon("Image file", "🖼️") }, 126 + { ".ico", AbbrIcon("Image file", "🖼️") }, 127 + { ".heic", AbbrIcon("Image file", "🖼️") }, 128 + { ".heif", AbbrIcon("Image file", "🖼️") }, 129 + { ".jpe?g", AbbrIcon("Image file", "🖼️") }, 130 + { ".jfif", AbbrIcon("Image file", "🖼️") }, 131 + { ".jxl", AbbrIcon("Image file", "🖼️") }, 132 + { ".j2c", AbbrIcon("Image file", "🖼️") }, 133 + { ".jp2", AbbrIcon("Image file", "🖼️") }, 134 + { ".a?png", AbbrIcon("Image file", "🖼️") }, 135 + { ".svg", AbbrIcon("Image file", "🖼️") }, 136 + { ".tiff?", AbbrIcon("Image file", "🖼️") }, 137 + { ".webp", AbbrIcon("Image file", "🖼️") }, 138 + { ".pdn", AbbrIcon("Image file", "🖼️") }, 139 + { ".psd", AbbrIcon("Image file", "🖼️") }, 140 + { ".xcf", AbbrIcon("Image file", "🖼️") }, 141 + 142 + { ".ass", AbbrIcon("Subtitle file", "💬") }, 143 + { ".lrc", AbbrIcon("Subtitle file", "💬") }, 144 + { ".srt", AbbrIcon("Subtitle file", "💬") }, 145 + { ".srv3", AbbrIcon("Subtitle file", "💬") }, 146 + { ".ssa", AbbrIcon("Subtitle file", "💬") }, 147 + { ".vtt", AbbrIcon("Subtitle file", "💬") }, 148 + 149 + { ".bat", AbbrIcon("Windows script file", "📜") }, 150 + { ".cmd", AbbrIcon("Windows script file", "📜") }, 151 + { ".htm", AbbrIcon("HTML file", "📜") }, 152 + { ".html", AbbrIcon("HTML file", "📜") }, 153 + { ".xhtml", AbbrIcon("XHTML file", "📜") }, 154 + { ".bash", AbbrIcon("Shell script", "📜") }, 155 + { ".zsh", AbbrIcon("Shell script", "📜") }, 156 + { ".sh", AbbrIcon("Shell script", "📜") }, 157 + { ".cpp", AbbrIcon("C++ source file", "📜") }, 158 + { ".cxx", AbbrIcon("C++ source file", "📜") }, 159 + { ".cc", AbbrIcon("C++ source file", "📜") }, 160 + { ".hpp", AbbrIcon("C++ header file", "📜") }, 161 + { ".hxx", AbbrIcon("C++ header file", "📜") }, 162 + { ".hh", AbbrIcon("C++ header file", "📜") }, 163 + 164 + { ".py", AbbrIcon("Python script", "📜") }, 165 + { ".pyc", AbbrIcon("Compiled Python bytecode", "📜") }, 166 + { ".pyo", AbbrIcon("Compiled Python bytecode", "📜") }, 167 + { ".psm1", AbbrIcon("PowerShell module file", "📜") }, 168 + { ".psd1", AbbrIcon("PowerShell data file", "📜") }, 169 + { ".ps1", AbbrIcon("PowerShell script", "📜") }, 170 + { ".js", AbbrIcon("JavaScript source code", "📜") }, 171 + { ".css", AbbrIcon("CSS style sheet", "📜") }, 172 + { ".cs", AbbrIcon("C# source file", "📜") }, 173 + { ".c", AbbrIcon("C source file", "📜") }, 174 + { ".h", AbbrIcon("C header file", "📜") }, 175 + { ".java", AbbrIcon("Java source file", "📜") }, 176 + 177 + { ".json", AbbrIcon("Data/config file", "📜") }, 178 + { ".json5", AbbrIcon("Data/config file", "📜") }, 179 + { ".xml", AbbrIcon("Data/config file", "📜") }, 180 + { ".yaml", AbbrIcon("Data/config file", "📜") }, 181 + { ".yml", AbbrIcon("Data/config file", "📜") }, 182 + { ".ini", AbbrIcon("Data/config file", "📜") }, 183 + { ".toml", AbbrIcon("Data/config file", "📜") }, 184 + { ".cfg", AbbrIcon("Data/config file", "📜") }, 185 + { ".conf", AbbrIcon("Data/config file", "📜") }, 186 + { ".plist", AbbrIcon("Data/config file", "📜") }, 187 + { ".csv", AbbrIcon("Data/config file", "📜") }, 188 + 189 + { ".tar", AbbrIcon("File archive", "📦") }, 190 + { ".ar", AbbrIcon("File archive", "📦") }, 191 + { ".7z", AbbrIcon("File archive", "📦") }, 192 + { ".arc", AbbrIcon("File archive", "📦") }, 193 + { ".cab", AbbrIcon("File archive", "📦") }, 194 + { ".rar", AbbrIcon("File archive", "📦") }, 195 + { ".zip", AbbrIcon("File archive", "📦") }, 196 + { ".bz2", AbbrIcon("File archive", "📦") }, 197 + { ".gz", AbbrIcon("File archive", "📦") }, 198 + { ".lz", AbbrIcon("File archive", "📦") }, 199 + { ".lzma", AbbrIcon("File archive", "📦") }, 200 + { ".lzo", AbbrIcon("File archive", "📦") }, 201 + { ".xz", AbbrIcon("File archive", "📦") }, 202 + { ".Z", AbbrIcon("File archive", "📦") }, 203 + { ".zst", AbbrIcon("File archive", "📦") }, 204 + 205 + { ".apk", AbbrIcon("Android package", "📦") }, 206 + { ".deb", AbbrIcon("Debian package", "📦") }, 207 + { ".rpm", AbbrIcon("RPM package", "📦") }, 208 + { ".ipa", AbbrIcon("iOS/iPadOS package", "📦") }, 209 + { ".AppImage", AbbrIcon("AppImage bundle", "📦") }, 210 + { ".jar", AbbrIcon("Java archive", "☕") }, 211 + 212 + { ".dmg", AbbrIcon("Disk image", "💿") }, 213 + { ".iso", AbbrIcon("Disk image", "💿") }, 214 + { ".img", AbbrIcon("Disk image", "💿") }, 215 + { ".wim", AbbrIcon("Disk image", "💿") }, 216 + { ".esd", AbbrIcon("Disk image", "💿") }, 217 + 218 + 219 + { ".docx", AbbrIcon("Document", "📃") }, 220 + { ".doc", AbbrIcon("Document", "📃") }, 221 + { ".odt", AbbrIcon("Document", "📃") }, 222 + { ".pptx", AbbrIcon("Presentation", "📃") }, 223 + { ".ppt", AbbrIcon("Presentation", "📃") }, 224 + { ".odp", AbbrIcon("Presentation", "📃") }, 225 + { ".xslx", AbbrIcon("Spreadsheet", "📃") }, 226 + { ".xsl", AbbrIcon("Spreadsheet", "📃") }, 227 + { ".ods", AbbrIcon("Spreadsheet", "📃") }, 228 + { ".pdf", AbbrIcon("PDF", "📃") }, 229 + { ".md", AbbrIcon("Markdown document", "📃") }, 230 + { ".rst", AbbrIcon("reStructuredText document", "📃") }, 231 + { ".epub", AbbrIcon("EPUB e-book file", "📃") }, 232 + { ".log", AbbrIcon("Log file", "📃") }, 233 + { ".txt", AbbrIcon("Text file", "📃") }, 234 + 235 + { ".tff", AbbrIcon("Font file", "🗛") }, 236 + { ".otf", AbbrIcon("Font file", "🗛") }, 237 + { ".woff", AbbrIcon("Font file", "🗛") }, 238 + { ".woff2", AbbrIcon("Font file", "🗛") }, 239 + 240 + { ".mpls", AbbrIcon("Playlist file", "🎶") }, 241 + { ".m3u", AbbrIcon("Playlist file", "🎶") }, 242 + { ".m3u8", AbbrIcon("Playlist file", "🎶") }, 243 + 244 + { ".exe", AbbrIcon("Generic executable", "🔳") }, 245 + { ".elf", AbbrIcon("Generic executable", "🔳") }, 246 + { ".msi", AbbrIcon("Generic executable", "🔳") }, 247 + { ".msix", AbbrIcon("Generic executable", "🔳") }, 248 + { ".msixbundle", AbbrIcon("Generic executable", "🔳") }, 249 + { ".appx", AbbrIcon("Generic executable", "🔳") }, 250 + { ".appxbundle", AbbrIcon("Generic executable", "🔳") }, 251 + 252 + { ".dll", AbbrIcon("Dynamic library", "⚙️") }, 253 + { ".so", AbbrIcon("Dynamic library", "⚙️") }, 254 + { ".dylib", AbbrIcon("Dynamic library", "⚙️") } 255 + #endregion 256 + }; 257 + 258 + public static MarkupString GetIconForFileType(FileSystemInfo fsi) { 259 + if (fsi is DirectoryInfo dir) { 260 + return AbbrIcon("Directory", "📁"); 261 + } 262 + 263 + if (ExtToAbbr.TryGetValue(fsi.Name, out var abbr)) 264 + return ExtToAbbr[fsi.Extension]; 265 + 266 + return fsi.EuidAccess_R_X() 267 + ? AbbrIcon("Generic executable (+x)", "🔳") 268 + : AbbrIcon("File", "📄"); 269 + } 270 + 271 + extension<T>(IEnumerable<T> enumerable) { 272 + public T RandomElement() { 273 + int index = (new Random()).Next(0, enumerable.Count()); 274 + return enumerable.ElementAt(index); 275 + } 276 + } 277 + 278 + #pragma warning disable CA2101 279 + // DO NOT mark these as `CharSet = CharSet.Unicode`, this will break things!! 280 + [DllImport("libc.so.6")] 281 + private static extern int euidaccess(String pathname, int mode); 282 + 283 + [DllImport("libc.so.6", SetLastError=true)] 284 + private static extern String? realpath(String pathname, IntPtr resolved); 285 + #pragma warning restore CA2101 286 + 287 + private const int R_OK = 0b0100; 288 + private const int W_OK = 0b0010; 289 + private const int X_OK = 0b0001; 290 + 291 + extension(FileSystemInfo fsi) { 292 + public bool EuidAccess_R() { 293 + if (OperatingSystem.IsWindows()) { 294 + throw new NotImplementedException("No libc (euidaccess) on Windows!"); 295 + } 296 + 297 + return euidaccess(fsi.FullName, R_OK) == 0; 298 + } 299 + 300 + public bool EuidAccess_R_X() { 301 + if (OperatingSystem.IsWindows()) { 302 + throw new NotImplementedException("No libc (euidaccess) on Windows!"); 303 + } 304 + 305 + return euidaccess(fsi.FullName, R_OK | X_OK) == 0; 306 + } 307 + 308 + public bool IsReadable() { 309 + if (fsi is DirectoryInfo dir) { 310 + return dir.EuidAccess_R_X(); 311 + } 312 + 313 + return fsi.EuidAccess_R(); 314 + } 315 + 316 + // wrapper(win)/replacement(*nix) for ResolveLinkTarget 317 + // if not link, return self 318 + // see https://github.com/PowerShell/PowerShell/issues/25724 319 + public FileSystemInfo? ReadLink() { 320 + if (!fsi.Attributes.HasFlag(FileAttributes.ReparsePoint)) { 321 + return null; 322 + } 323 + 324 + if (OperatingSystem.IsWindows()) { 325 + return fsi.ResolveLinkTarget(true)!; 326 + } 327 + 328 + String? real = realpath(fsi.FullName, IntPtr.Zero); 329 + if (real == null) { 330 + // realpath(3): ... otherwise, it returns NULL, the contents of the array 331 + // resolved_path are undefined, and errno is set to indicate the error. 332 + int errno = Marshal.GetLastPInvokeError(); 333 + String msg = Marshal.GetLastPInvokeErrorMessage(); 334 + throw new Exception($"realpath for {fsi.FullName} failed with errno {errno}: {msg}"); 335 + } 336 + 337 + if (Directory.Exists(real)) { 338 + return new DirectoryInfo(real); 339 + } 340 + 341 + // the file at real might not actually exist, but we want to return it anyways 342 + return new FileInfo(real); 343 + } 344 + } 345 + 346 + extension(RenderFragment fragment) { 347 + public String RenderString() { 348 + StringBuilder builder = new StringBuilder(); 349 + RenderTreeBuilder renderer = new RenderTreeBuilder(); 350 + fragment(renderer); 351 + 352 + renderer.GetFrames().Array.ToList() 353 + .ForEach(f => { 354 + // this seems like the only types that have actual content? 355 + // idk tho 356 + switch (f.FrameType) { 357 + case RenderTreeFrameType.Markup: 358 + builder.Append(f.MarkupContent); 359 + break; 360 + case RenderTreeFrameType.Text: 361 + builder.Append(f.TextContent); 362 + break; 363 + default: 364 + break; 365 + } 366 + }); 367 + 368 + return builder.ToString(); 369 + } 370 + } 371 + }
+8
appsettings.Development.json
··· 1 + { 2 + "Logging": { 3 + "LogLevel": { 4 + "Default": "Information", 5 + "Microsoft.AspNetCore": "Warning" 6 + } 7 + } 8 + }
+12
appsettings.json
··· 1 + { 2 + "Logging": { 3 + "LogLevel": { 4 + "Default": "Information", 5 + "Microsoft.AspNetCore": "Warning" 6 + } 7 + }, 8 + "AllowedHosts": "*", 9 + "DoNotServe": [ 10 + "*cookies.txt" // NOT READ!!! 11 + ] 12 + }
-42
errors/default.html.ps1
··· 1 - <# 2 - This file is part of Utatane. 3 - 4 - Utatane is free software: you can redistribute it and/or modify it under the 5 - 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 - $Root = $Data.Root 19 - $Path = $Data.Path 20 - 21 - $TheFunny = "OOPSIE WOOPSIE!! Uwu We made a fucky wucky!! A wittle fucko boingo! The code monkeys at our headquarters are working VEWY HAWD to fix this!" 22 - 23 - ./templates/frame.ps1 ` 24 - -IsError ` 25 - -Title $TheFunny ` 26 - -Body { 27 - div -Class "n-box text-center gap-2" { 28 - h1 { 29 - span -Class 'text-6xl' { $Data.Status.Code } 30 - br 31 - span -Class 'text-4xl' { $Data.Status.Description } 32 - } 33 - 34 - hr 35 - 36 - $TheFunny 37 - 38 - hr 39 - 40 - a -Href '/' -Class 'clickable p-4' { 'go home......' } 41 - } 42 - }
+9 -11
errors/default.json.ps1 Components/App.razor
··· 1 - <# 1 + @* 2 2 This file is part of Utatane. 3 - 4 - Utatane is free software: you can redistribute it and/or modify it under the 5 - terms of the GNU Affero General Public License as published by the Free 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 6 Software Foundation, either version 3 of the License, or (at your option) 7 7 any later version. 8 - 8 + 9 9 Utatane is distributed in the hope that it will be useful, but WITHOUT ANY 10 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 11 FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 12 12 more details. 13 - 13 + 14 14 You should have received a copy of the GNU Affero General Public License 15 15 along with Utatane. If not, see <http://www.gnu.org/licenses/>. 16 - #> 16 + *@ 17 17 18 - @{ 19 - Code = $Data.Status.Code 20 - Description = $Data.Status.Description 21 - } 18 + @* see Layout/MainLayout.razor for the <html>... *@ 19 + <Routes/>
-635
functions.ps1
··· 1 - <# 2 - This file is part of Utatane. 3 - 4 - Utatane is free software: you can redistribute it and/or modify it under the 5 - 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 - Import-Module Pode 19 - Import-Module Mizumiya 20 - 21 - # WARNING: modifying this requires restarting the entire shell 22 - Add-Type -Namespace Native -Name LibC -MemberDefinition @' 23 - [DllImport("libc.so.6")] 24 - public static extern int euidaccess(string pathname, int mode); 25 - 26 - [DllImport("libc.so.6", SetLastError=true)] 27 - public static extern IntPtr readlink(string pathname, byte[] buf, UIntPtr bufsize); 28 - '@ 29 - 30 - function _can_r_x ([IO.FileSystemInfo] $Path) { 31 - return ([Native.LibC]::euidaccess($Path.FullName, 5 <# R_OK -bor X_OK #>) -eq 0) 32 - } 33 - 34 - function _can_r__ ([IO.FileSystemInfo] $Path) { 35 - return ([Native.LibC]::euidaccess($Path.FullName, 4 <# R_OK #>) -eq 0) 36 - } 37 - 38 - function _readlink { 39 - param ( 40 - [String] $PathName 41 - ) 42 - 43 - if ([String]::IsNullOrWhitespace($PathName)) { 44 - throw [ArgumentException]::new("PathName cannot be null!") 45 - } 46 - 47 - $BufSize = 1024 48 - $Buf = [Array]::CreateInstance([byte], $BufSize) 49 - 50 - $BufMax = [Native.LibC]::readlink($PathName, $Buf, [UIntPtr]$BufSize).ToInt64() 51 - 52 - if ($BufMax -lt 0) { 53 - $errno = [System.Runtime.InteropServices.Marshal]::GetLastPInvokeError() 54 - $strerror = [System.Runtime.InteropServices.Marshal]::GetLastPInvokeErrorMessage() 55 - throw [Exception]::new("readlink failed with errno $errno`: $strerror") 56 - } 57 - 58 - return [System.Text.Encoding]::Default.GetString($Buf, 0, $BufMax) 59 - } 60 - 61 - function _log { 62 - param( 63 - [Parameter(ValueFromPipeline)] 64 - $Message, 65 - [ValidateSet('Request', 'Information', 'Warning', 'Fatal')] 66 - $Type 67 - ) 68 - 69 - switch ($Type) { 70 - 'Request' { 71 - $Style = $PSStyle.Foreground.BrightBlack 72 - } 73 - 74 - 'Information' { 75 - $Style = '' 76 - } 77 - 78 - 'Warning' { 79 - $Style = $PSStyle.Formatting.Warning 80 - } 81 - 82 - 'Fatal' { 83 - $Style = $PSStyle.Formatting.Error 84 - } 85 - } 86 - 87 - Write-Host "$Style[Utatane/$Type] $Message$($PSStyle.Reset)" 88 - if ($Type -eq 'Fatal') { throw $Message } 89 - } 90 - 91 - function _request { 92 - param( 93 - [Parameter(ValueFromPipeline)] 94 - $Message 95 - ) 96 - 97 - _log -Type Request -Message $Message 98 - } 99 - 100 - function _info { 101 - param( 102 - [Parameter(ValueFromPipeline)] 103 - $Message 104 - ) 105 - 106 - _log -Type Information -Message $Message 107 - } 108 - 109 - function _warn { 110 - param( 111 - [Parameter(ValueFromPipeline)] 112 - $Message 113 - ) 114 - 115 - _log -Type Warning -Message $Message 116 - } 117 - 118 - function _fatal { 119 - param ( 120 - [Parameter(ValueFromPipeline)] 121 - $Message 122 - ) 123 - 124 - _log -Type Fatal $Message 125 - } 126 - 127 - function ConvertFrom-Markdown2 { 128 - param ( 129 - $Path, 130 - $Location, 131 - $Root 132 - ) 133 - $Location = (gi $Location).FullName 134 - 135 - $eee = ($Location -replace "^$Root", '' -split '/')[1..1024] # whatever 136 - $LinkRoot = $Root 137 - 138 - # try to find .git in 139 - for ($i=$eee.count ; $i -gt 0 ; $i--) { 140 - $TestPath = $eee[0..($i-1)] -join '/' 141 - 142 - if (Test-Path -LiteralPath "$Root/$TestPath/.git") { 143 - $LinkRoot = "/$TestPath" 144 - break; 145 - } 146 - } 147 - 148 - $Markdown = gc -lit $Path 149 - 150 - # fix naked roots that only work on github by defining the link root 151 - # surely this will not break anything 152 - $Markdown = $Markdown -replace '\]\(\/', "]($Root/" 153 - 154 - # look for a link with no colon (using this a proxy for having a protocol AKA not being a domain-relative path) 155 - $Markdown = $Markdown -replace '(?<=\]\().*?(?=\))', { 156 - $_ -notlike '*:*' ? "$LinkRoot/$_" : $_ 157 - } 158 - 159 - # look closer: some READMEs decide to use full html in them, so we have to detect that as well... 160 - $Markdown = $Markdown -replace '(?<=href=\x22|src=\x22).*?(?=\x22)', { 161 - $_ -like '*/*' ? "$LinkRoot/$_" : $_ 162 - } 163 - 164 - return (ConvertFrom-Markdown -InputObject ($Markdown -join "`n")).Html 165 - } 166 - 167 - # check if path is a file or a symlink pointing to a file test-path seems to 168 - # have an issue with strange file names, like "[̸D̶A̴T̷A̸ ̴E̸X̷P̷U̴N̸G̶E̶D̸]̷" 169 - # this is also an order of magnitude faster than test-path 170 - function _is_file ([IO.FileSystemInfo] $Path) { 171 - return ( 172 - # [IO.File]::Exists($Path) -or [IO.File]::Exists($Path.linktarget) 173 - # $null -ne $Path -and $Path.GetType() -eq [IO.FileInfo] 174 - $null -ne $Path -and -not $Path.PSIsContainer 175 - ) 176 - } 177 - 178 - function _is_readable ([IO.FileSystemInfo] $Path) { 179 - # directories can only be "read" if we also have x permission 180 - return ((_is_file $Path) ? (_can_r__ $Path) : (_can_r_x $Path)) 181 - } 182 - 183 - # encode string as uri 184 - function _encode { 185 - param ( 186 - [String] $part 187 - ) 188 - 189 - return [URI]::EscapeDataString($part) 190 - } 191 - 192 - # seperately encode all parts of the url path to avoid encoding the slash or 193 - # something silly 194 - function _encode_path { 195 - param ( 196 - [String] $Path, 197 - [Switch] $NoEnd 198 - ) 199 - 200 - if ($NoEnd) { 201 - $FixedPath = (_encode_path (( $Path -split '/' | Select-Object -SkipLast 1) -Join '/')) 202 - if ($FixedPath -eq [String]::Empty) { 203 - return '/' 204 - } else { 205 - return $FixedPath 206 - } 207 - } else { 208 - return ($Path -split '/' | % { _encode $_ }) -join '/' 209 - } 210 - } 211 - 212 - function _get_splash { 213 - (Get-PodeConfig).Utatane.Splashes | Get-Random | ConvertFrom-Markdown | % Html 214 - } 215 - 216 - function _format_size ([uint64] $size) { 217 - switch ($Size) { 218 - {$size -ge 1tb} { return '{0:n2} TiB' -f ($size/1tb) } 219 - {$size -ge 1gb} { return '{0:n2} GiB' -f ($size/1gb) } 220 - {$size -ge 1mb} { return '{0:n2} MiB' -f ($size/1mb) } 221 - {$size -ge 1kb} { return '{0:n2} KiB' -f ($size/1kb) } 222 - {$size -eq '-'} { return '0 B' } 223 - default { return "$size B" } 224 - } 225 - } 226 - 227 - filter _check_path_on_blacklist ($Path) { 228 - ((Get-PodeConfig).Utatane.DoNotServe | ? { $Path -like $_ }).Count -gt 0 229 - } 230 - 231 - function _check_path ($Root, $FullPath) { 232 - $Resolved = Get-Item -LiteralPath $FullPath -EA Ignore 233 - 234 - return ( 235 - $null -ne $Resolved ` 236 - -and $Resolved.FullName.StartsWith($Root) ` 237 - -and !(_check_path_on_blacklist $Resolved.Name) ` 238 - ) 239 - } 240 - 241 - function _script_hx_hs_jq { 242 - script -Src /.nhnd/_hyperscript.js 243 - script -Src /.nhnd/_hs_tailwind.js 244 - script -Src /.nhnd/htmx.js 245 - script -Src /.nhnd/jquery.js 246 - meta -Name htmx-config -Content (@{ scrollIntoViewOnBoost=$false; defaultHideShowStrategy='twDisplay' } | ConvertTo-Json -Compress) 247 - } 248 - 249 - function _footer { 250 - footer -Class 'pb-10 pt-5' { 251 - span -Class 'justify-center w-full flex text-current/75 gap-[1ch]' { 252 - '🌟🌟🌟' 253 - a -Href '/about' { 'running helpimnotdrowning/Utatane' } 254 - '🌟🌟🌟' 255 - } 256 - } 257 - } 258 - 259 - function _icon ($Label, $Icon) { 260 - return abbr -Title $Label { 261 - $Icon 262 - } 263 - } 264 - 265 - $MapExtensionToAbbr = @{ 266 - '.avc' = ( _icon 'Video file' '🎞️' ) 267 - '.flv' = ( _icon 'Video file' '🎞️' ) 268 - '.mts' = ( _icon 'Video file' '🎞️' ) 269 - '.m2ts' = ( _icon 'Video file' '🎞️' ) 270 - '.m4v' = ( _icon 'Video file' '🎞️' ) 271 - '.mkv' = ( _icon 'Video file' '🎞️' ) 272 - '.mov' = ( _icon 'Video file' '🎞️' ) 273 - '.mp4' = ( _icon 'Video file' '🎞️' ) 274 - '.ts' = ( _icon 'Video file' '🎞️' ) 275 - '.webm' = ( _icon 'Video file' '🎞️' ) 276 - '.wmv' = ( _icon 'Video file' '🎞️' ) 277 - 278 - '.aac' = ( _icon 'Audio file' '🔊' ) 279 - '.alac' = ( _icon 'Audio file' '🔊' ) 280 - '.flac' = ( _icon 'Audio file' '🔊' ) 281 - '.m4a' = ( _icon 'Audio file' '🔊' ) 282 - '.mp3' = ( _icon 'Audio file' '🔊' ) 283 - '.opus' = ( _icon 'Audio file' '🔊' ) 284 - '.wav' = ( _icon 'Audio file' '🔊' ) 285 - '.ogg' = ( _icon 'Audio file' '🔊' ) 286 - '.mus' = ( _icon 'Audio file' '🔊' ) 287 - 288 - '.avif' = ( _icon 'Image file' '🖼️' ) 289 - '.bmp' = ( _icon 'Image file' '🖼️' ) 290 - '.gif' = ( _icon 'Image file' '🖼️' ) 291 - '.ico' = ( _icon 'Image file' '🖼️' ) 292 - '.heic' = ( _icon 'Image file' '🖼️' ) 293 - '.heif' = ( _icon 'Image file' '🖼️' ) 294 - '.jpe?g' = ( _icon 'Image file' '🖼️' ) 295 - '.jfif' = ( _icon 'Image file' '🖼️' ) 296 - '.jxl' = ( _icon 'Image file' '🖼️' ) 297 - '.j2c' = ( _icon 'Image file' '🖼️' ) 298 - '.jp2' = ( _icon 'Image file' '🖼️' ) 299 - '.a?png' = ( _icon 'Image file' '🖼️' ) 300 - '.svg' = ( _icon 'Image file' '🖼️' ) 301 - '.tiff?' = ( _icon 'Image file' '🖼️' ) 302 - '.webp' = ( _icon 'Image file' '🖼️' ) 303 - '.pdn' = ( _icon 'Image file' '🖼️' ) 304 - '.psd' = ( _icon 'Image file' '🖼️' ) 305 - '.xcf' = ( _icon 'Image file' '🖼️' ) 306 - 307 - '.ass' = ( _icon 'Subtitle file' '💬' ) 308 - '.lrc' = ( _icon 'Subtitle file' '💬' ) 309 - '.srt' = ( _icon 'Subtitle file' '💬' ) 310 - '.srv3' = ( _icon 'Subtitle file' '💬' ) 311 - '.ssa' = ( _icon 'Subtitle file' '💬' ) 312 - '.vtt' = ( _icon 'Subtitle file' '💬' ) 313 - 314 - '.bat' = ( _icon 'Windows script file' '📜' ) 315 - '.cmd' = ( _icon 'Windows script file' '📜' ) 316 - '.htm' = ( _icon 'HTML file' '📜' ) 317 - '.html' = ( _icon 'HTML file' '📜' ) 318 - '.xhtml' = ( _icon 'XHTML file' '📜' ) 319 - '.bash' = ( _icon 'Shell script' '📜' ) 320 - '.zsh' = ( _icon 'Shell script' '📜' ) 321 - '.sh' = ( _icon 'Shell script' '📜' ) 322 - '.cpp' = ( _icon 'C++ source file' '📜' ) 323 - '.cxx' = ( _icon 'C++ source file' '📜' ) 324 - '.cc' = ( _icon 'C++ source file' '📜' ) 325 - '.hpp' = ( _icon 'C++ header file' '📜' ) 326 - '.hxx' = ( _icon 'C++ header file' '📜' ) 327 - '.hh' = ( _icon 'C++ header file' '📜' ) 328 - 329 - '.py' = ( _icon 'Python script' '📜' ) 330 - '.pyc' = ( _icon 'Compiled Python bytecode' '📜' ) 331 - '.pyo' = ( _icon 'Compiled Python bytecode' '📜' ) 332 - '.psm1' = ( _icon 'PowerShell module file' '📜' ) 333 - '.psd1' = ( _icon 'PowerShell data file' '📜' ) 334 - '.ps1' = ( _icon 'PowerShell script' '📜' ) 335 - '.js' = ( _icon 'JavaScript source code' '📜' ) 336 - '.css' = ( _icon 'CSS style sheet' '📜' ) 337 - '.cs' = ( _icon 'C# source file' '📜' ) 338 - '.c' = ( _icon 'C source file' '📜' ) 339 - '.h' = ( _icon 'C header file' '📜' ) 340 - '.java' = ( _icon 'Java source file' '📜' ) 341 - 342 - '.json' = ( _icon 'Data/config file' '📜' ) 343 - '.json5' = ( _icon 'Data/config file' '📜' ) 344 - '.xml' = ( _icon 'Data/config file' '📜' ) 345 - '.yaml' = ( _icon 'Data/config file' '📜' ) 346 - '.yml' = ( _icon 'Data/config file' '📜' ) 347 - '.ini' = ( _icon 'Data/config file' '📜' ) 348 - '.toml' = ( _icon 'Data/config file' '📜' ) 349 - '.cfg' = ( _icon 'Data/config file' '📜' ) 350 - '.conf' = ( _icon 'Data/config file' '📜' ) 351 - '.plist' = ( _icon 'Data/config file' '📜' ) 352 - '.csv' = ( _icon 'Data/config file' '📜' ) 353 - 354 - '.tar' = ( _icon 'File archive' '📦' ) 355 - '.ar' = ( _icon 'File archive' '📦' ) 356 - '.7z' = ( _icon 'File archive' '📦' ) 357 - '.arc' = ( _icon 'File archive' '📦' ) 358 - '.cab' = ( _icon 'File archive' '📦' ) 359 - '.rar' = ( _icon 'File archive' '📦' ) 360 - '.zip' = ( _icon 'File archive' '📦' ) 361 - '.bz2' = ( _icon 'File archive' '📦' ) 362 - '.gz' = ( _icon 'File archive' '📦' ) 363 - '.lz' = ( _icon 'File archive' '📦' ) 364 - '.lzma' = ( _icon 'File archive' '📦' ) 365 - '.lzo' = ( _icon 'File archive' '📦' ) 366 - '.xz' = ( _icon 'File archive' '📦' ) 367 - '.Z' = ( _icon 'File archive' '📦' ) 368 - '.zst' = ( _icon 'File archive' '📦' ) 369 - 370 - '.apk' = ( _icon 'Android package' '📦' ) 371 - '.deb' = ( _icon 'Debian package' '📦' ) 372 - '.rpm' = ( _icon 'RPM package' '📦' ) 373 - '.ipa' = ( _icon 'iOS/iPadOS package' '📦' ) 374 - '.AppImage' = ( _icon 'AppImage bundle' '📦' ) 375 - '.jar' = ( _icon 'Java archive' '☕' ) 376 - 377 - '.dmg' = ( _icon 'Disk image' '💿' ) 378 - '.iso' = ( _icon 'Disk image' '💿' ) 379 - '.img' = ( _icon 'Disk image' '💿' ) 380 - '.wim' = ( _icon 'Disk image' '💿' ) 381 - '.esd' = ( _icon 'Disk image' '💿' ) 382 - 383 - 384 - '.docx' = ( _icon 'Document' '📃' ) 385 - '.doc' = ( _icon 'Document' '📃' ) 386 - '.odt' = ( _icon 'Document' '📃' ) 387 - '.pptx' = ( _icon 'Presentation' '📃' ) 388 - '.ppt' = ( _icon 'Presentation' '📃' ) 389 - '.odp' = ( _icon 'Presentation' '📃' ) 390 - '.xslx' = ( _icon 'Spreadsheet' '📃' ) 391 - '.xsl' = ( _icon 'Spreadsheet' '📃' ) 392 - '.ods' = ( _icon 'Spreadsheet' '📃' ) 393 - '.pdf' = ( _icon 'PDF' '📃' ) 394 - '.md' = ( _icon 'Markdown document' '📃' ) 395 - '.rst' = ( _icon 'reStructuredText document' '📃' ) 396 - '.epub' = ( _icon 'EPUB e-book file' '📃' ) 397 - '.log' = ( _icon 'Log file' '📃' ) 398 - '.txt' = ( _icon 'Text file' '📃' ) 399 - 400 - '.tff' = ( _icon 'Font file' '🗛' ) 401 - '.otf' = ( _icon 'Font file' '🗛' ) 402 - '.woff' = ( _icon 'Font file' '🗛' ) 403 - '.woff2' = ( _icon 'Font file' '🗛' ) 404 - 405 - '.mpls' = ( _icon 'Playlist file' '🎶' ) 406 - '.m3u' = ( _icon 'Playlist file' '🎶' ) 407 - '.m3u8' = ( _icon 'Playlist file' '🎶' ) 408 - 409 - '.exe' = ( _icon 'Generic executable' '🔳' ) 410 - '.elf' = ( _icon 'Generic executable' '🔳' ) 411 - '.msi' = ( _icon 'Generic executable' '🔳' ) 412 - '.msix' = ( _icon 'Generic executable' '🔳' ) 413 - '.msixbundle' = ( _icon 'Generic executable' '🔳' ) 414 - '.appx' = ( _icon 'Generic executable' '🔳' ) 415 - '.appxbundle' = ( _icon 'Generic executable' '🔳' ) 416 - 417 - '.dll' = ( _icon 'Dynamic library' '⚙️' ) 418 - '.so' = ( _icon 'Dynamic library' '⚙️' ) 419 - '.dylib' = ( _icon 'Dynamic library' '⚙️' ) 420 - } 421 - function _get_symbol_for_file ([IO.FileSystemInfo] $FSO) { 422 - if (-not (_is_file $FSO)) { 423 - _icon 'Directory' '📁' 424 - } else { 425 - $Icon = $MapExtensionToAbbr[$FSO.Extension] 426 - 427 - if ([String]::IsNullOrEmpty($Icon)) { 428 - if (-not $IsWindows -and (_can_r_x $FSO)) { 429 - return _icon 'Generic executable (+x)' '🔳' 430 - } else { 431 - return _icon 'File' '📄' 432 - } 433 - } 434 - return $Icon 435 - } 436 - } 437 - 438 - function _create_table_row ([IO.FileSystemInfo] $Item, [String] $CurrentPath) { 439 - $IsFile = _is_file $Item 440 - 441 - try { 442 - # if Item is a symlink, canonicalize it 443 - if ($null -ne $Item.LinkTarget) { 444 - # ResolvedTarget is broken, see https://github.com/PowerShell/PowerShell/issues/25724 445 - $LinkDestination = Get-Item -LiteralPath (readlink $Item.FullName -m) -ErrorAction Ignore 446 - 447 - if ($null -eq $LinkDestination) { 448 - throw [System.NullReferenceException]::new("Broken symbolic link at $($Item.FullName)") 449 - } 450 - 451 - # size of symlinks shows the size of the link, not the file iself! 452 - $Size = $LinkDestination.Size 453 - } else { 454 - $Size = $Item.Size 455 - } 456 - 457 - if (-not (_is_readable $Item)) { 458 - return ( 459 - tr { 460 - td 461 - 462 - td { _icon "Server does not have permission to read this $($IsFile ? 'file' : 'directory')" '⚠️' } 463 - 464 - td -Class "file-name" -Attributes @{ "data-order" = $Item.Name } { 465 - s -Class 'text-stone-500' { $IsFile ? $Item.Name : $Item.Name + '/' } 466 - } 467 - 468 - td -Class 'file-size' -Attributes @{ 'data-order' = ($IsFile ? $Size : 0) } { 469 - if ($IsFile) { s { _format_size ([uint64] $Size) } } 470 - } 471 - 472 - td -Class 'file-date' -Attributes @{ 'data-order' = ( Get-Date $Item.LastWriteTime -UFormat "%s" -asutc ) } { 473 - s { 474 - time { 475 - Get-Date $Item.LastWriteTime -Format "yyyy-MM-dd HH:mm" -AsUTC 476 - } 477 - } 478 - } 479 - } 480 - ) 481 - } 482 - 483 - # Mizumiya can be slow when tens of thousands of elements are needed 484 - # (case: ~39,100 to generate ~3900 rows of file information), so 485 - # templating is used here. this achieves a 2x speedup (12s -> 5s) in 486 - # exchange for readability. 487 - 488 - $FilePath = _encode_path (Join-Path ($CurrentPath ? $CurrentPath : '/') $Item.Name) 489 - return ( 490 - @" 491 - <tr class="fsobject $( $IsFile ? "file" : "directory" )"> 492 - <td>$( if ($IsFile) { @" 493 - <a 494 - download 495 - class="file-dl clickable" 496 - href="$FilePath" 497 - aria-label="Download file">⭳</a> 498 - "@ } )</td> 499 - <td>$( _get_symbol_for_file $Item )</td> 500 - <td class=file-name data-order="$( AttributeEncode $Item.Name )"> 501 - $( if ($IsFile) { @" 502 - <a 503 - href="$FilePath" 504 - hx-boost=false 505 - >$( HTMLEncode { $Item.Name } )</a> 506 - "@ } else { @" 507 - <a 508 - href="$FilePath/" 509 - hx-boost=true 510 - $(<# string are added bc using a ; seperator will join with a space #>) 511 - $(<# this will not #>) 512 - >$( (HTMLEncode $Item.Name) + (span -Class 'dir-slash' -InnerHTML '/') )</a> 513 - "@ } ) 514 - </td> 515 - <td 516 - class=file-size 517 - data-order=$($IsFile ? $Size : -1) 518 - >$( $IsFile ? (_format_size ([uint64] $Size)) : '' )</td> 519 - <td 520 - class=file-date 521 - data-order=$( Get-Date $Item.LastWriteTime -UFormat "%s" -AsUTC ) 522 - > 523 - <time> 524 - $( Get-Date $Item.LastWriteTime -Format "yyyy-MM-dd HH:mm" -AsUTC ) 525 - </time> 526 - </td> 527 - </tr> 528 - "@ 529 - ) 530 - } catch { 531 - _warn "api/files.html: Row generation for $($Item.FullName) failed!!" 532 - _warn $_.Exception 533 - 534 - return ( 535 - tr { 536 - td 537 - 538 - td { _icon 'Error occured while generating this row' '⚠️' } 539 - 540 - td -Class "file-name" -Attributes @{ "data-order" = $Item.Name } { 541 - s { $Item.Name } 542 - } 543 - 544 - td 545 - 546 - td 547 - } 548 - ) 549 - } 550 - } 551 - 552 - function Get-PathParent { 553 - [CmdletBinding()] 554 - param ( 555 - [String] $Path 556 - ) 557 - 558 - $Parent = $Path | % split '/' | Select-Object -SkipLast 1 | Join-String -Separator '/' 559 - 560 - return ($Parent ? '/' : $Parent) 561 - } 562 - 563 - function ConvertFrom-AcceptHeader { 564 - param ( 565 - [Parameter(ValueFromPipeline)] 566 - [String] $AcceptString 567 - ) 568 - 569 - if ([String]::IsNullOrWhiteSpace($AcceptString)) { 570 - return [PSCustomObject]@{ 571 - Quality = 1d 572 - MediaType = '*/*' 573 - } 574 - } 575 - 576 - return $AcceptString -split ',' | % { 577 - try { 578 - $Accept = [System.Net.Http.Headers.MediaTypeWithQualityHeaderValue]::Parse($_) 579 - return [PSCustomObject]@{ 580 - Quality = ($null -eq $Accept.Quality) ? 1d : $Accept.Quality 581 - MediaType = $Accept.MediaType 582 - } 583 - } catch { 584 - return; 585 - } 586 - } | Sort-Object -Descending -Property Quality 587 - } 588 - 589 - function _mime_to_ext { 590 - param( 591 - [String] $Mime 592 - ) 593 - 594 - switch ($Mime) { 595 - 'text/html' { 'html' } 596 - 'application/json' { 'json' } 597 - 'application/xml' { 'xml' } 598 - 'text/xml' { 'xml' } 599 - 'application/xhtml+xml' { 'xhtml' } 600 - 'application/atom+xml' { 'atom' } 601 - default { 'html' } 602 - } 603 - } 604 - 605 - function _render_view { 606 - param( 607 - [String] $Page, 608 - $Data 609 - ) 610 - 611 - :loop foreach ($Accept in $WebEvent.RequestAccept) { 612 - # SEE `Set-PodeViewEngine -Type Mizumiya` for the ACTUAL work being done 613 - switch ( _mime_to_ext $Accept.MediaType ) { 614 - 'json' { 615 - Write-PodeViewResponse -Path "$Page.json.ps1" -Data $Data 616 - break loop 617 - } 618 - 619 - 'html' { 620 - Write-PodeViewResponse -Path "$Page.html.ps1" -Data $Data 621 - break loop 622 - } 623 - 624 - 'xml' { 625 - Write-PodeViewResponse -Path "$Page.xml.ps1" -Data $Data 626 - break loop 627 - } 628 - 629 - 'atom' { 630 - Write-PodeViewResponse -Path "$Page.atom.ps1" -Data $Data 631 - break loop 632 - } 633 - } 634 - } 635 - }
+26 -141
public/style.css
··· 1 - /*! tailwindcss v4.1.14 | MIT License | https://tailwindcss.com */ 1 + /*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */ 2 2 @layer properties; 3 3 @layer theme, base, components, utilities; 4 4 @layer theme { ··· 10 10 Consolas, "Liberation Mono", "Courier New", monospace, 11 11 "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", 12 12 "Noto Color Emoji"; 13 - --color-red-400: oklch(70.4% 0.191 22.216); 14 - --color-red-500: oklch(63.7% 0.237 25.331); 15 13 --color-green-100: oklch(96.2% 0.044 156.743); 16 14 --color-green-200: oklch(92.5% 0.084 155.995); 17 15 --color-green-300: oklch(87.1% 0.15 154.449); ··· 27 25 --color-zinc-500: oklch(55.2% 0.016 285.938); 28 26 --color-zinc-600: oklch(44.2% 0.017 285.786); 29 27 --color-stone-400: oklch(70.9% 0.01 56.259); 30 - --color-stone-500: oklch(55.3% 0.013 58.071); 31 28 --color-black: #000; 32 29 --color-white: #fff; 33 30 --spacing: 0.25rem; 34 31 --container-6xl: 72rem; 35 - --text-4xl: 2.25rem; 36 - --text-4xl--line-height: calc(2.5 / 2.25); 37 - --text-6xl: 3.75rem; 38 - --text-6xl--line-height: 1; 39 32 --font-weight-bold: 700; 40 33 --radius-sm: 0.25rem; 41 34 --drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1); ··· 156 149 ::placeholder { 157 150 color: currentcolor; 158 151 @supports (color: color-mix(in lab, red, red)) { 159 - & { 160 - color: color-mix(in oklab, currentcolor 50%, transparent); 161 - } 152 + color: color-mix(in oklab, currentcolor 50%, transparent); 162 153 } 163 154 } 164 155 } ··· 197 188 display: none !important; 198 189 } 199 190 } 200 - @layer utilities { 201 - .pointer-events-none { 202 - pointer-events: none; 203 - } 204 - .static { 205 - position: static; 206 - } 207 - .my-auto { 208 - margin-block: auto; 209 - } 210 - .flex { 211 - display: flex; 212 - } 213 - .hidden { 214 - display: none; 215 - } 216 - .table { 217 - display: table; 218 - } 219 - .w-full { 220 - width: 100%; 221 - } 222 - .cursor-default\! { 223 - cursor: default !important; 224 - } 225 - .flex-col { 226 - flex-direction: column; 227 - } 228 - .flex-row { 229 - flex-direction: row; 230 - } 231 - .justify-center { 232 - justify-content: center; 233 - } 234 - .gap-2 { 235 - gap: calc(var(--spacing) * 2); 236 - } 237 - .gap-\[1ch\] { 238 - gap: 1ch; 239 - } 240 - .border-black { 241 - border-color: var(--color-black); 242 - } 243 - .from-red-400\! { 244 - --tw-gradient-from: var(--color-red-400) !important; 245 - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)) !important; 246 - } 247 - .to-red-500\! { 248 - --tw-gradient-to: var(--color-red-500) !important; 249 - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)) !important; 250 - } 251 - .p-4 { 252 - padding: calc(var(--spacing) * 4); 253 - } 254 - .pt-5 { 255 - padding-top: calc(var(--spacing) * 5); 256 - } 257 - .pb-10 { 258 - padding-bottom: calc(var(--spacing) * 10); 259 - } 260 - .text-center { 261 - text-align: center; 262 - } 263 - .\[vertical-align\:sub\] { 264 - vertical-align: sub; 265 - } 266 - .font-mono { 267 - font-family: var(--font-mono); 268 - } 269 - .text-4xl { 270 - font-size: var(--text-4xl); 271 - line-height: var(--tw-leading, var(--text-4xl--line-height)); 272 - } 273 - .text-6xl { 274 - font-size: var(--text-6xl); 275 - line-height: var(--tw-leading, var(--text-6xl--line-height)); 276 - } 277 - .text-current\/75 { 278 - color: currentcolor; 279 - @supports (color: color-mix(in lab, red, red)) { 280 - & { 281 - color: color-mix(in oklab, currentcolor 75%, transparent); 282 - } 283 - } 284 - } 285 - .text-stone-500 { 286 - color: var(--color-stone-500); 287 - } 288 - .filter { 289 - filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); 290 - } 291 - } 191 + @layer utilities; 292 192 @font-face { 293 193 font-family: IBM Plex Mono; 294 194 font-style: normal; ··· 3078 2978 &:hover { 3079 2979 background-color: color-mix(in srgb, oklch(70.7% 0.165 254.624) 15%, transparent); 3080 2980 @supports (color: color-mix(in lab, red, red)) { 3081 - & { 3082 - background-color: color-mix(in oklab, var(--color-blue-400) 15%, transparent); 3083 - } 2981 + background-color: color-mix(in oklab, var(--color-blue-400) 15%, transparent); 3084 2982 } 3085 2983 } 3086 2984 color: var(--color-blue-500); ··· 3182 3080 &:hover td { 3183 3081 background-color: color-mix(in srgb, oklch(92.5% 0.084 155.995) 75%, transparent); 3184 3082 @supports (color: color-mix(in lab, red, red)) { 3185 - & { 3186 - background-color: color-mix(in oklab, var(--color-green-200) 75%, transparent); 3187 - } 3083 + background-color: color-mix(in oklab, var(--color-green-200) 75%, transparent); 3188 3084 } 3189 3085 @media (prefers-color-scheme: dark) { 3190 3086 background-color: color-mix(in srgb, oklch(70.5% 0.015 286.067) 75%, transparent); 3191 3087 @supports (color: color-mix(in lab, red, red)) { 3192 - & { 3193 - background-color: color-mix(in oklab, var(--color-zinc-400) 75%, transparent); 3194 - } 3088 + background-color: color-mix(in oklab, var(--color-zinc-400) 75%, transparent); 3195 3089 } 3196 3090 } 3197 3091 } ··· 3204 3098 tr:hover td { 3205 3099 background-color: color-mix(in srgb, oklch(96.2% 0.044 156.743) 50%, transparent); 3206 3100 @supports (color: color-mix(in lab, red, red)) { 3207 - & { 3208 - background-color: color-mix(in oklab, var(--color-green-100) 50%, transparent); 3209 - } 3101 + background-color: color-mix(in oklab, var(--color-green-100) 50%, transparent); 3210 3102 } 3211 3103 --tw-duration: 0ms; 3212 3104 transition-duration: 0ms; 3213 3105 @media (prefers-color-scheme: dark) { 3214 3106 background-color: color-mix(in srgb, oklch(55.2% 0.016 285.938) 75%, transparent); 3215 3107 @supports (color: color-mix(in lab, red, red)) { 3216 - & { 3217 - background-color: color-mix(in oklab, var(--color-zinc-500) 75%, transparent); 3218 - } 3108 + background-color: color-mix(in oklab, var(--color-zinc-500) 75%, transparent); 3219 3109 } 3220 3110 } 3221 3111 } ··· 3355 3245 s { 3356 3246 color: currentcolor; 3357 3247 @supports (color: color-mix(in lab, red, red)) { 3358 - & { 3359 - color: color-mix(in oklab, currentcolor 40%, transparent); 3360 - } 3248 + color: color-mix(in oklab, currentcolor 40%, transparent); 3361 3249 } 3362 3250 } 3363 3251 & th { ··· 3391 3279 font-family: var(--font-mono); 3392 3280 & a { 3393 3281 display: inline-block; 3394 - width: 100%; 3395 3282 padding-block: calc(var(--spacing) * 0); 3396 3283 } 3397 3284 } ··· 3438 3325 &#filetable-head-time, &#filetable-head-size, &#filetable-head-name { 3439 3326 color: currentcolor; 3440 3327 @supports (color: color-mix(in lab, red, red)) { 3441 - & { 3442 - color: color-mix(in oklab, currentcolor 25%, transparent); 3443 - } 3328 + color: color-mix(in oklab, currentcolor 25%, transparent); 3444 3329 } 3445 3330 } 3446 3331 } ··· 3512 3397 inherits: false; 3513 3398 initial-value: 100%; 3514 3399 } 3400 + @property --tw-leading { 3401 + syntax: "*"; 3402 + inherits: false; 3403 + } 3404 + @property --tw-duration { 3405 + syntax: "*"; 3406 + inherits: false; 3407 + } 3408 + @property --tw-border-style { 3409 + syntax: "*"; 3410 + inherits: false; 3411 + initial-value: solid; 3412 + } 3515 3413 @property --tw-blur { 3516 3414 syntax: "*"; 3517 3415 inherits: false; ··· 3565 3463 syntax: "*"; 3566 3464 inherits: false; 3567 3465 } 3568 - @property --tw-leading { 3569 - syntax: "*"; 3570 - inherits: false; 3571 - } 3572 - @property --tw-duration { 3573 - syntax: "*"; 3574 - inherits: false; 3575 - } 3576 - @property --tw-border-style { 3577 - syntax: "*"; 3578 - inherits: false; 3579 - initial-value: solid; 3580 - } 3581 3466 @property --tw-font-weight { 3582 3467 syntax: "*"; 3583 3468 inherits: false; ··· 3599 3484 --tw-gradient-from-position: 0%; 3600 3485 --tw-gradient-via-position: 50%; 3601 3486 --tw-gradient-to-position: 100%; 3487 + --tw-leading: initial; 3488 + --tw-duration: initial; 3489 + --tw-border-style: solid; 3602 3490 --tw-blur: initial; 3603 3491 --tw-brightness: initial; 3604 3492 --tw-contrast: initial; ··· 3612 3500 --tw-drop-shadow-color: initial; 3613 3501 --tw-drop-shadow-alpha: 100%; 3614 3502 --tw-drop-shadow-size: initial; 3615 - --tw-leading: initial; 3616 - --tw-duration: initial; 3617 - --tw-border-style: solid; 3618 3503 --tw-font-weight: initial; 3619 3504 } 3620 3505 }
-46
server.psd1
··· 1 - <# 2 - This file is part of Utatane. 3 - 4 - Utatane is free software: you can redistribute it and/or modify it under the 5 - 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 - @{ 19 - Utatane = ( Microsoft.Powershell.Utility\Import-PowerShellDataFile ./Utatane.psd1 ) 20 - 21 - Server = @{ 22 - AllowedActions = @{ 23 - Suspend = $false 24 - Disable = $false 25 - } 26 - } 27 - 28 - Web = @{ 29 - Compression = @{ 30 - Enable = $true 31 - } 32 - 33 - ErrorPages = @{ 34 - StrictContentTyping = $true 35 - } 36 - 37 - Static = @{ 38 - Cache = @{ 39 - Enable = $true 40 - Include = @( 41 - '/.nhnd/*' 42 - ) 43 - } 44 - } 45 - } 46 - }
-1
tailwind/style.tw.css Tailwind/style.tw.css
··· 384 384 & a { 385 385 @apply 386 386 inline-block 387 - w-full 388 387 py-0; 389 388 } 390 389 }
-46
templates/frame.ps1
··· 1 - <# 2 - This file is part of Utatane. 3 - 4 - Utatane is free software: you can redistribute it and/or modify it under the 5 - 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 - param( 19 - [Switch] $IsError, 20 - [ScriptBlock] $Head, 21 - [String]$Title, 22 - 23 - [ScriptBlock] $Body 24 - ) 25 - 26 - doctype 27 - 28 - html { 29 - head { 30 - link -Rel stylesheet -Href '/.nhnd/style.css' 31 - title $Title 32 - 33 - _script_hx_hs_jq 34 - 35 - if ($Head) { 36 - $Head.Invoke() 37 - } 38 - } 39 - 40 - body -Class "flex flex-col $($IsError ? 'from-red-400! to-red-500!' : $null)" { 41 - if ($Body) { 42 - $Body.Invoke() 43 - } 44 - _footer 45 - } 46 - }
-37
views/about.html.ps1
··· 1 - <# 2 - This file is part of Utatane. 3 - 4 - Utatane is free software: you can redistribute it and/or modify it under the 5 - 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 - <# ### ### ### #> 19 - 20 - . ./templates/frame.ps1 ` 21 - -Title "About" ` 22 - -Body { 23 - div -Class 'n-box flex flex-row' { 24 - div -Class "flex" { 25 - a -Href '/' -Class 'clickable' -HxBoost true { '/' } 26 - } 27 - 28 - hr 29 - 30 - @" 31 - The source code for Utatane (this site) is available on the 32 - [helpimnotdrowning Git forge](https://git.helpimnotdrowning.net/helpimnotdrowning/Utatane) 33 - and [GitHub](https://github.com/helpimnotdrowning/Utatane). It is served and 34 - redistributed under the AGPLv3 license. 35 - "@ | ConvertFrom-Markdown | % Html 36 - } 37 - }
-126
views/api/files.html.ps1
··· 1 - <# 2 - This file is part of Utatane. 3 - 4 - Utatane is free software: you can redistribute it and/or modify it under the 5 - 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 - $Root = $Data.Root 19 - $CurrentPath = ($WebEvent.Query.Path -eq '/') ? "" : $WebEvent.Query.Path 20 - $JoinedPath = $Data.JoinedPath 21 - 22 - $SortDir = $Webevent.query['sort-asc'] -eq 'on' ? 'asc' : 'desc' 23 - 24 - $SortBy = switch ($WebEvent.Query['sort-by']) { 25 - "name" { "Name" } 26 - "size" { "Size" } 27 - "time" { "LastWriteTime" } 28 - default { "Name" } 29 - } 30 - 31 - $ForceThreadedRenderer = $WebEvent.Query.threaded_renderer -eq 'true' ? $true : $false 32 - 33 - $Listing = Get-ChildItem -Lit $JoinedPath | ? { -not (_check_path_on_blacklist $_) } 34 - $MaxSize = 100 35 - $BatchSize = 1000 36 - $IsFirst = $true 37 - 38 - <# ### ### ### #> 39 - 40 - if (-not (_is_readable (Get-Item $JoinedPath))) { 41 - return ( 42 - tr { 43 - td 44 - td { _icon "Server does not have permission to read this directory" '⚠️' } 45 - td { 'Server does not have permission to read this directory' } 46 - td 47 - td 48 - } 49 - ) 50 - } 51 - 52 - if ($Listing.Count -gt ($MaxSize * 2)) { 53 - $Timer = [System.Diagnostics.Stopwatch]::new() 54 - $Timer.Start(); 55 - 56 - # Optimization for massive file listings: split them up & do batch processing 57 - $BatchList = [System.Collections.Generic.List[Array]]::new() 58 - $Rows = [System.Collections.Concurrent.ConcurrentBag[hashtable]]::new() 59 - 60 - for ($i=0; $i -lt [Math]::floor(($Listing.Count / $BatchSize) + 1); $i++) { 61 - $BatchList.Add(@( $Listing | Select-Object -Skip ($i * $BatchSize) -First $BatchSize )) 62 - } 63 - 64 - "Starting fast renderer with $($BatchList.Count) batches"|Write-Host 65 - 66 - $BatchList | % -ThrottleLimit (nproc) -Parallel { 67 - . ./functions.ps1 68 - 69 - $ID = New-GUID 70 - 71 - $Batch = $_ 72 - 73 - _info "running batch $ID (size:$($Batch.Count))" 74 - 75 - $Batch | % { 76 - $Entry = $_ 77 - ($using:Rows).Add(@{ 78 - Row = (_create_table_row $Entry $using:CurrentPath) 79 - Entry = $Entry 80 - }) 81 - } 82 - 83 - _info "Finished batch $ID (size:$($Batch.Count))" 84 - } 85 - 86 - $Rows.ToArray() | Sort-Object -Prop ` 87 - @{ Expression={ _is_file $_.Entry }; Descending=$False }, ` 88 - @{ Expression={ $_.Entry.$SortBy }; Descending=($SortDir -eq "desc") }, ` 89 - @{ Expression={ $_.Entry.Name }; Descending=$False} 90 - | % Row 91 - 92 - $Timer.stop() 93 - _warn "Fast renderer took $Timer for $($Listing.Count) items (in $($Batch.Count) batches)" 94 - } else { 95 - $Timer = [System.Diagnostics.Stopwatch]::new() 96 - $Timer.Start(); 97 - 98 - $ListingSorted = $Listing | Sort-Object -Prop ` 99 - @{ Expression={ _is_file $_ }; Descending=$False}, ` 100 - @{ Expression={ $_.$SortBy }; Descending=($SortDir -eq "desc") }, ` 101 - @{ Expression={ $_.Name }; Descending=$False} 102 - 103 - $IsFirst = $true 104 - $ListingSorted | % { 105 - # bruh 106 - if (-not $IsFirst) { 107 - _create_table_row $_ $CurrentPath 108 - } else { 109 - _create_table_row $ListingSorted[0] $CurrentPath 110 - $IsFirst = $false 111 - } 112 - } 113 - 114 - $Timer.Stop() 115 - _info "Basic render took $Timer for $($Listing.Count) items" 116 - } 117 - 118 - function _sorter { 119 - param ( 120 - $GetEntry 121 - ) 122 - 123 - return ` 124 - @{ Expression={ _is_file (& $GetEntry $_) }; Descending=$False} , ` 125 - @{ Expression={ $_.$SortBy }; Descending=($SortDir -eq "desc") } 126 - }
-40
views/api/files.json.ps1
··· 1 - <# 2 - This file is part of Utatane. 3 - 4 - Utatane is free software: you can redistribute it and/or modify it under the 5 - 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 - param ( 18 - $Data 19 - ) 20 - 21 - $Root = $Data.Root 22 - $CurrentPath = ($WebEvent.Query.Path -eq '/') ? "" : $WebEvent.Query.Path 23 - $JoinedPath = $Data.JoinedPath 24 - 25 - <# ### ### ### #> 26 - Get-ChildItem -Lit $JoinedPath | ? { -not (_check_path_on_blacklist $_) } | % { 27 - # bug ???? 28 - if ($_ -isnot [IO.FileSystemInfo]) { 29 - return 30 - } 31 - 32 - $is_file = _is_file $_ 33 - 34 - @{ 35 - Type = $is_file ? 'file' : 'directory' 36 - Name = $_.Name 37 - Size = $is_file ? $_.size : $null # check if works for symlinks!! 38 - LastModified = [uint64](Get-Date $_.LastWriteTime -UFormat '%s' -AsUTC) 39 - } 40 - }
-269
views/index.html.ps1
··· 1 - <# 2 - This file is part of Utatane. 3 - 4 - Utatane is free software: you can redistribute it and/or modify it under the 5 - 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 - $Root = $Data.Root 19 - $Path = $Data.Path 20 - $JoinedPath = Join-Path $Root $Path 21 - 22 - $RedirectNotice = Get-PodeFlashMessage -Name redirect-notice 23 - 24 - <# ### ### ### #> 25 - 26 - . ./templates/frame.ps1 ` 27 - -Title "Index: $Path" ` 28 - -Head { 29 - script -Type text/hyperscript { @" 30 - def get_dir(x) 31 - if not x 32 - return "descending" 33 - otherwise 34 - return "ascending" 35 - end 36 - end 37 - 38 - behavior sort_table(property) 39 - on click 40 - -- wait 0.1s 41 - remove @aria-sort from <th/> 42 - 43 - set target to the first <#prop-`${property}/> 44 - set dir_target to the first <input[name="sort-asc"]/> 45 - 46 - if (target's checked is true) 47 - set dir_target's checked to (not dir_target's checked) 48 - otherwise 49 - set <input[name=sort-by]/>'s checked to false 50 - set dir_target's checked to true 51 - set target's checked to true 52 - end 53 - 54 - set my @aria-sort to get_dir((dir_target's checked)) 55 - send nhnd:run to me 56 - end 57 - end 58 - "@ 59 - } 60 - } ` 61 - -Body { 62 - header -Class 'n-box' { 63 - div -Id 'breadcrumbs' { 64 - a -Href '/' -Class 'clickable' -HxBoost true { '/' } 65 - 66 - $PathFragments = $Path -split '/' | Select-Object -Skip 1 -SkipLast 1 67 - if ($PathFragments -eq '') { 68 - return 69 - } 70 - 71 - $PathFragments | % { 72 - $CurrentCrumb = _encode_path ($PathFragments | Select-Object -first (++$i) | Join-String -Sep '/') 73 - a -Href "/$CurrentCrumb" -Class 'clickable' -HxBoost true { $_ } 74 - } | Join-String -Separator (span '/') 75 - } 76 - 77 - div -Id 'toolbar' { 78 - button -Id reload-table ` 79 - -Class clickable ` 80 - -Type button ` 81 - -AriaLabel 'Reload table' ` 82 - -_hs @" 83 - -- shortcut: only the active <th> will have aria-sort 84 - on click send nhnd:run to <th[aria-sort]/> 85 - "@ ` 86 - { span -Class '[vertical-align:sub]' { '↻' } } 87 - 88 - $IsAtRoot = ($Path -eq '/') 89 - a -Id go-back ` 90 - -Class clickable ` 91 - -AriaLabel 'Go up 1 directory' ` 92 - -HxBoost true ` 93 - -Attr @{ 94 - {aria-disabled} = ($IsAtRoot ? "true" : "false") 95 - tabindex = $IsAtRoot ? -1 : 0 96 - onClick = $IsAtRoot ? 'event.preventDefault();' : '' 97 - href = $IsAtRoot ? $null : '..' 98 - } ` 99 - { span -Class 'cursor-default! [vertical-align:sub]' { '../' } } 100 - 101 - abbr -id search-label -Title 'Search' { '🔎' } 102 - input ` 103 - -Id search ` 104 - -Class border-black ` 105 - -Type search ` 106 - -AriaLabelledBy search-label ` 107 - -_hs @' 108 - on load 109 - set my value to '' 110 - on input 111 - if (#use-rx-for-search's checked is true) then 112 - make a RegExp from my value called rxSearch 113 - show <tbody > tr/> in #filetable when rxSearch.test(the @data-order of .file-name in it) is true 114 - otherwise 115 - show <tbody > tr/> in #filetable when (the first @data-order of .file-name in it) contains my value.toLowerCase() 116 - end 117 - on keydown from body 118 - make a RegExp from "^(F|Soft)\\d" called rx 119 - if not ( 120 - document.activeElement is me or -- dont repeat it! 121 - event.key is "" or -- actually I forgot 122 - event.ctrlKey or event.altKey or event.metaKey or -- dont react on modifier combos 123 - ["Shift", "Tab", "ArrowUp", "ArrowRight", "ArrowLeft", "ArrowDown", 124 - "AltGraph", "CapsLock", "Fn", "FnLock", "Hyper", "NumLock", 125 - "ScrollLock", "Super", "Symbol", "SymbolLock", "OS", "End", "Home", 126 - "PageDown", "PageUp", "Backspace", "Clear", "Delete", "Redo", 127 - "Undo", "Accept", "Again", "Attn", "Cancel", "ContextMenu", 128 - "Escape", "Find", "Pause", "Play", "Props", "Select", "ZoomIn", 129 - "ZoomOut", "Apps", "BrightnessDown", "BrightnessUp", "PrintScreen", 130 - "MediaFastForward", "MediaPause", "MediaPlay", "MediaPlayPause", 131 - "MediaRecord", "MediaRewind", "MediaStop", "MediaTrackNext", 132 - "MediaTrackPrevious"].includes(event.key) or -- special keys to not react on 133 - rx.test(event.key) -- more special keys but these can be regexed 134 - ) 135 - go to the top of the body 136 - focus() on me 137 - '@ 138 - abbr -Id rx-label -Title 'Search with RegEx' { '.*' } 139 - input ` 140 - -Id use-rx-for-search ` 141 - -Type checkbox ` 142 - -Autocomplete off ` 143 - -Checked ` 144 - -AriaLabelledBy rx-label ` 145 - -_hs @" 146 - on load or click -- on load for if this comes checked 147 - if < #use-rx-for-search:checked />'s length 148 - add .font-mono to #search 149 - otherwise 150 - remove .font-mono from #search 151 - end 152 - send input to #search 153 - "@ 154 - 155 - span -Id 'file-counter' -Class 'my-auto' -_hs @" 156 - on nhnd:update 157 - set files to `$('.file').length 158 - set dirs to `$('.directory').length 159 - set my innerHTML to ```${dirs} $(_icon 'Directories' '📁') `${files} $(_icon 'Files' '📄')`` 160 - "@ 161 - } 162 - } 163 - 164 - if ($null -ne $RedirectNotice) { 165 - span -Id redirect-notice -Class 'n-box flex text-center' { 166 - @" 167 - **Notice**: The path ``$($RedirectNotice.From)`` now redirects to ``$($RedirectNotice.To)``. 168 - Change any bookmarks for the old address to this new address, ``$($WebEvent.Request.Url)`` 169 - "@ | ConvertFrom-Markdown | % Html 170 - } 171 - } 172 - 173 - div -Class 'n-box flex flex-row' { 174 - div -Id 'main' { 175 - form -Id 'sorting-on' -Class 'hidden' { 176 - input -Autocomplete off -Type radio -Name sort-by -Value name -Id prop-name -Checked 177 - input -Autocomplete off -Type radio -Name sort-by -Value size -Id prop-size 178 - input -Autocomplete off -Type radio -Name sort-by -Value time -Id prop-time 179 - 180 - input -Autocomplete off -Type checkbox -Name sort-asc -Checked 181 - } 182 - 183 - table ` 184 - -Id 'filetable' ` 185 - -_hs "on htmx:afterSettle send nhnd:update to #file-counter" ` 186 - { 187 - thead { 188 - tr -HxInclude '#sorting-on input' { 189 - function install_sort($prop) { "install sort_table(property: '$prop') end" } 190 - $EncodedPath = _encode $Path 191 - 192 - th -Id filetable-head-dl ` 193 - -Class 'pointer-events-none' ` 194 - -TabIndex 0 ` 195 - { span -Class 'visually-hidden' {'Download link'} } 196 - 197 - th -Id filetable-head-icon ` 198 - -Class 'pointer-events-none' ` 199 - -TabIndex 0 ` 200 - { span -Class 'visually-hidden' {'File type icon'} } 201 - 202 - th -Id filetable-head-name ` 203 - -HxTrigger 'load from:document, nhnd:run' ` 204 - -HxGet "/api/files?path=$EncodedPath" ` 205 - -HxTarget '#filetable tbody' ` 206 - -HxSwap innerHTML ` 207 - -HxIndicator '#filetable-body, #name-spinner, #filetable-head-name' ` 208 - -AriaSort ascending ` 209 - -_hs (install_sort name) ` 210 - { 211 - div { 212 - span -Class header-label { 213 - 'Name' 214 - span -Id name-spinner -Class spinner 215 - } 216 - } 217 - } 218 - 219 - th -Id filetable-head-size ` 220 - -HxTrigger 'nhnd:run' ` 221 - -HxGet "/api/files?path=$EncodedPath" ` 222 - -HxTarget '#filetable tbody' ` 223 - -HxSwap innerHTML ` 224 - -HxIndicator '#filetable-body, #size-spinner, #filetable-head-size' ` 225 - -_hs (install_sort size) ` 226 - { 227 - div { 228 - span -Class header-label { 229 - 'Size' 230 - span -Id size-spinner -Class spinner 231 - } 232 - } 233 - } 234 - 235 - th -Id filetable-head-time ` 236 - -HxTrigger 'nhnd:run' ` 237 - -HxGet "/api/files?path=$EncodedPath" ` 238 - -HxTarget '#filetable tbody' ` 239 - -HxSwap innerHTML ` 240 - -HxIndicator '#filetable-body, #time-spinner, #filetable-head-time' ` 241 - -_hs (install_sort time) ` 242 - { 243 - div { 244 - span -Class header-label { 245 - 'Last Modified' 246 - span -Id time-spinner -Class spinner 247 - } 248 - } 249 - 250 - } 251 - } 252 - } 253 - 254 - tbody -Id 'filetable-body' -Class 'htmx-indicator' { 255 - # pregen rows to prevent massive layout shift 256 - gci -lit $JoinedPath | % { 257 - tr { 258 - td 259 - td 260 - td -Class "" { "`u{00A0}" } 261 - td 262 - td 263 - } 264 - } 265 - } 266 - } 267 - } 268 - } 269 - }
+9 -15
views/index.json.ps1 Components/Layout/EmptyLayout.razor
··· 1 - <# 1 + @* 2 2 This file is part of Utatane. 3 - 4 - Utatane is free software: you can redistribute it and/or modify it under the 5 - terms of the GNU Affero General Public License as published by the Free 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 6 Software Foundation, either version 3 of the License, or (at your option) 7 7 any later version. 8 - 8 + 9 9 Utatane is distributed in the hope that it will be useful, but WITHOUT ANY 10 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 11 FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 12 12 more details. 13 - 13 + 14 14 You should have received a copy of the GNU Affero General Public License 15 15 along with Utatane. If not, see <http://www.gnu.org/licenses/>. 16 - #> 17 - 18 - $Root = $Data.Root 19 - $Path = $Data.Path 20 - $JoinedPath = Join-Path $Root $Path 16 + *@ 21 17 22 - <# ### ### ### #> 18 + @inherits LayoutComponentBase 23 19 24 - @{ 25 - Message = "Query /api/files?path=$(_encode $Path) with header Accept: application/json" 26 - } 20 + @Body