Nice little directory browser :D
at master 198 lines 5.6 kB view raw
1@* 2 This file is part of Utatane. 3 4 Utatane is free software: you can redistribute it and/or modify it under 5 the terms of the GNU Affero General Public License as published by the Free 6 Software Foundation, either version 3 of the License, or (at your option) 7 any later version. 8 9 Utatane is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for 12 more details. 13 14 You should have received a copy of the GNU Affero General Public License 15 along with Utatane. If not, see <http://www.gnu.org/licenses/>. 16*@ 17 18@page "/api/files" 19@using System.Diagnostics.CodeAnalysis 20@layout EmptyLayout 21 22<AppTitle Partial>@( Path == "/" ? "/" : Path.Split('/').Last() )</AppTitle> 23 24<input autocomplete="off" type="text" name="path" value="@Path" id="prop-path" hx-swap-oob="true" /> 25 26<Breadcrumbs Path="@Path" hx-swap-oob="true"/> 27 28<GoBackButton Path="@Path" hx-swap-oob="true" /> 29 30<table> 31<tbody> 32@foreach (var row in _rows) { 33 <tr class="fsobject @row.TypeString"> 34 <td> 35 @if (row.IsFile && !row.IsBad) { 36 <a 37 download 38 class="file-dl clickable" 39 href="@row.Href" 40 aria-label="Download file">⭳</a> 41 } 42 </td> 43 <td>@row.Icon</td> 44 <td class="file-name" data-order="@row.Name"> 45 @if (row.IsBad) { 46 <s class="text-stone-500"> 47 @(row.Name + row.Trail) 48 </s> 49 } else { 50 @if (row.IsFile) { 51 <a class="@(row.IsDotFile ? "dotfile" : null)" 52 href="@(row.Href + row.Trail)" 53 > 54 @row.Name 55 </a> 56 } else { 57 <a class="@(row.IsDotFile ? "dotfile" : null)" 58 href="@(row.Href + row.Trail)" 59 hx-get="/api/files?path=@row.Href" 60 hx-target="#filetable tbody" 61 hx-swap="innerHtml scroll:top" 62 hx-push-url="@(row.Href + row.Trail)" 63 hx-indicator="#filetable-body, #name-spinner, #filetable-head-time" 64 > 65 @row.Name<span class="dir-slash">/</span> 66 </a> 67 } 68 } 69 </td> 70 <td class="file-size" data-order="@row.Size"> 71 @if (row.IsFile) { 72 @if (row.IsBad) { 73 <s>@Utils.FormatFileSize(row.Size)</s> 74 } else { 75 @Utils.FormatFileSize(row.Size) 76 ; 77 } 78 } 79 </td> 80 <td class="file-date" data-order="@row.TimeUnix"> 81 @if (row.IsBad) { 82 <s><time>@row.TimeFmt</time></s> 83 } else { 84 <time>@row.TimeFmt</time> 85 } 86 </td> 87 </tr> 88} 89@* </tbody> *@ 90</tbody> 91</table> 92 93@code { 94 95 [SupplyParameterFromQuery(Name = "path")] 96 public required String? Path { get; set; } 97 98 [SupplyParameterFromQuery(Name = "sort-by")] 99 public required String? SortByColumn { get; set; } 100 101 [SupplyParameterFromQuery(Name = "sort-asc")] 102 public required String SortAsc { get; set; } 103 104 private DirectoryInfo _realPath; 105 private SortByColumns _sortByColumn; 106 private SortByDirection _sortByDirection; 107 private IEnumerable<FileRow> _rows; 108 109 protected override Task OnInitializedAsync() { 110 if (String.IsNullOrEmpty(Path)) 111 Path = "/"; 112 else if (Path.EndsWith("/") && Path != "/") 113 Path = Path.TrimEnd('/'); 114 115 // prechecked in middleware! 116 _realPath = Utils.VerifyPath(Path).Value.AsT1(); 117 _sortByColumn = SortByColumn switch { 118 "name" => SortByColumns.Name, 119 "size" => SortByColumns.Size, 120 "time" => SortByColumns.Time, 121 _ => SortByColumns.Name 122 }; 123 _sortByDirection = SortAsc switch { 124 "on" => SortByDirection.Ascending, 125 _ => SortByDirection.Descending 126 127 }; 128 _rows = _realPath.EnumerateFileSystemInfos() 129 .Select(fsi => new FileRow(fsi, Path ?? "/")) 130 .OrderByDescending(x => !x.IsFile) 131 .ThenBy(x => x.Name); 132 133 // Console.WriteLine($"Path <{Path}> | Col <{SortByColumn}> | Dir <{SortAsc}>"); 134 // Console.WriteLine($"RealPath <{_realPath}> | Col <{_sortByColumn}> | Dir <{_sortByDirection}>"); 135 136 return base.OnInitializedAsync(); 137 } 138 139 public enum SortByColumns { 140 Name, 141 Size, 142 Time, 143 } 144 145 public enum SortByDirection { 146 Ascending, 147 Descending, 148 } 149 150 public class FileRow { 151 public required FileSystemInfo Fsi { get; init; } 152 public required bool IsFile { get; init; } 153 public required bool IsBad { get; init; } 154 public required String TypeString { get; init; } 155 public required MarkupString Icon { get; init; } 156 public required String Name { get; init; } 157 public required bool IsDotFile { get; init; } 158 public required long Size { get; init; } 159 public required String TimeFmt { get; init; } 160 public required String TimeUnix { get; init; } 161 public required String? Href { get; init; } 162 public required char? Trail { get; init; } 163 164 [SetsRequiredMembers] 165 public FileRow(FileSystemInfo baseFsi, String currentPath) { 166 FileSystemInfo? _fsi; 167 bool _isBad = false; 168 169 try { 170 _fsi = baseFsi.UnravelLink(); 171 } catch { 172 _isBad = true; 173 _fsi = baseFsi; 174 } 175 176 Fsi = _fsi; 177 IsFile = Fsi is FileInfo; 178 IsBad = _isBad || !Fsi.IsReadable(); 179 TypeString = IsFile ? "file" : "directory"; 180 Icon = IsBad 181 ? Utils.AbbrIcon($"Server cannot read this {TypeString}", "⚠️") 182 : Utils.GetIconForFileType(Fsi); 183 Name = baseFsi.Name; // specifically want whatever its called in the directory we're looking at 184 IsDotFile = Name.StartsWith('.'); 185 Size = IsBad || !IsFile ? -1 : ((FileInfo)Fsi).Length; 186 TimeFmt = baseFsi.LastWriteTime.ToUniversalTime().ToString("yyyy-MM-dd HH:mm"); 187 TimeUnix = $"{baseFsi.LastWriteTime.ToUniversalTime().Subtract(DateTime.UnixEpoch).TotalSeconds:N0}"; 188 Href = IsBad 189 ? null 190 : String.Join('/', 191 System.IO.Path.Join(currentPath, Name) 192 .Split('/') 193 .Select(Uri.EscapeDataString) 194 ); 195 Trail = IsFile ? null : '/'; 196 } 197 } 198}