Nice little directory browser :D
1@*
2 This file is part of Utatane.
3
4 Utatane is free software: you can redistribute it and/or modify it under
5 the terms of the GNU Affero General Public License as published by the Free
6 Software Foundation, either version 3 of the License, or (at your option)
7 any later version.
8
9 Utatane is distributed in the hope that it will be useful, but WITHOUT ANY
10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
12 more details.
13
14 You should have received a copy of the GNU Affero General Public License
15 along with Utatane. If not, see <http://www.gnu.org/licenses/>.
16*@
17
18@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}