Nice little directory browser :D

Lots of changes, see rest of message * responses can now vary on the Accept header (currently just html/json) * accessing any normal path with Accept application/json will give instructions to api endpoint * remove `/list` endpoint (feature killed) * add simple `/about` * data-order of HTML directory row is now `-1`, from `0` * load config from `Utatane.psd1` instead of bundling everything in `server.psd1` * config `Utatane.DoNotServe` property, glob filename blacklist for files that will be hidden and not served * remove dumb "go up" button on error page * use tiny `frame.ps1` template in HTML views instead of doing the whole stucture in each one

+299 -183
+1
.gitignore
··· 1 + Utatane.psd1
+50 -51
Routes.ps1
··· 64 64 65 65 Set-PodeViewEngine -Type Mizumiya -Extension ps1 -ScriptBlock { 66 66 param ($Path, $Data) 67 + 67 68 . ./functions.ps1 68 69 69 - ([String] (. $Path $Data)) | Optimize-HTML 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 + } 70 102 } 71 103 72 104 $Middleware_DirsHaveTrailingSlash = { ··· 94 126 return $true 95 127 } 96 128 129 + Add-PodeMiddleware -Name CreateAcceptInWebEvent -ScriptBlock { 130 + $WebEvent.RequestAccept = ((Get-PodeHeader 'Accept') ?? 'text/html' | ConvertFrom-AcceptHeader) 131 + 132 + return $true 133 + } 134 + 97 135 Add-PodeRouteGroup -Path /api -Routes { 98 136 Add-PodeRoute -Path /files -Method GET -ScriptBlock { 99 137 $Root = $using:Root 100 138 $Path = $WebEvent.Query.Path 101 139 $JoinedPath = Join-Path $Root $Path 102 140 103 - if (-not (_check_joinedpath_is_based $Root $JoinedPath)) { 141 + if (-not (_check_path $Root $JoinedPath)) { 104 142 _warn "/api/files: query for $JoinedPath ($Path) refused!" 105 143 Set-PodeResponseStatus -Code 404 106 - return 144 + return; 107 145 } 108 146 109 147 $Data = @{ Root=$Root; Path=$Path; JoinedPath=$JoinedPath} 110 - switch ($WebEvent.ContentType) { 111 - 'application/json' { 112 - Write-PodeViewResponse -Path api/files.json.ps1 -Data $Data 113 - } 114 - 115 - default { 116 - Write-PodeViewResponse -Path api/files.html.ps1 -Data $Data 117 - } 118 - } 119 - 120 - return 121 - } 122 - 123 - Add-PodeRoute -Path /list -Method GET -ScriptBlock { 124 - $Root = $using:Root 125 - $Path = $WebEvent.Query.Path 126 - $JoinedPath = Join-Path $Root $Path 127 - 128 - if (-not (_check_joinedpath_is_based $Root $JoinedPath)) { 129 - _warn "/api/list: query for $JoinedPath ($Path) refused!" 130 - Set-PodeResponseStatus -Code 404 131 - return 132 - } 133 - 134 - Write-PodeViewResponse -Path api/list.html.ps1 -Data $Data 148 + _render_view -Page api/files -Data $Data 135 149 } 136 150 137 151 Add-PodeRoute -Path /* -Method GET -ScriptBlock { ··· 139 153 } 140 154 } 141 155 156 + Add-PodeRoute -Method GET -Path /about -ScriptBlock { 157 + _render_view about 158 + } 159 + 142 160 Add-PodeRoute -Method GET -Path /* -Middleware $Middleware_DirsHaveTrailingSlash -ScriptBlock { 143 161 $Root = $using:Root 144 162 $Path = $WebEvent.Path ?? '/' 145 163 $JoinedPath = Get-Item -Lit (Join-Path $Root $Path) 146 164 147 - # Make sure our root is actually available 148 - 149 - 150 165 if ($null -eq $JoinedPath) { 151 - # _warn "failed: '$(Join-Path $Root $Path)' for query $($WebEvent.Query|convertto-json -compress)" 166 + _warn "1failed: '$(Join-Path $Root $Path)' for query $($WebEvent.Query|convertto-json -compress)" 152 167 Set-PodeResponseStatus -Code 404 153 168 return 154 169 } 155 170 156 - # No path traversal! 157 - if (-not (_check_joinedpath_is_based $Root $JoinedPath)) { 171 + # No path traversal, no blacklist! 172 + if (-not (_check_path $Root $JoinedPath)) { 158 173 _warn "denied $Path ($JoinedPath)" 159 174 Set-PodeResponseStatus -Code 404 160 175 return 161 176 } 162 177 163 - # yt-dlp cookies file default name 164 - $DO_NOT_SERVE = @( '*cookies.txt', '*cookie.txt') 165 - $DO_NOT_SERVE | % { 166 - if ($JoinedPath -like $_) { 167 - _warn "failed, DO_NOT_SERVE: $Path ($JoinedPath)" 168 - Set-PodeResponseStatus -Code 404 169 - return 170 - } 171 - } 172 - 173 178 $IsFile = _is_file $JoinedPath 174 179 175 180 if ($IsFile) { 176 - _info here 177 181 Set-PodeResponseAttachment -path ([WildcardPattern]::Escape($JoinedPath)) 178 182 return 179 183 } else { ··· 183 187 Set-PodeResponseStatus -Code 404 184 188 } 185 189 186 - if ($WebEvent.ContentType -eq 'application/json') { 187 - Write-PodeViewResponse -Path api/files.json.ps1 -Data @{ Root=$Root } -ContentType application/json 188 - return 189 - } 190 - 191 - Write-PodeViewResponse -Path index -Data @{ Root=$Root ; Path=$Path } 190 + _render_view -Page index -Data @{ Root=$Root ; Path=$Path } 192 191 return 193 192 } 194 193
+6
Utatane.example.psd1
··· 1 + @{ 2 + Root = '/media/share' 3 + DoNotServe = @( 4 + # '*cookies.txt' 5 + ) 6 + }
+6 -19
errors/default.html.ps1
··· 18 18 $Root = $Data.Root 19 19 $Path = $Data.Path 20 20 21 - doctype 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 22 23 - html -class 'from-red-300! to-red-500!' { 24 - head { 25 - link -Rel stylesheet -Href '/style.css' 26 - } 27 - 28 - body { 23 + ./templates/frame.ps1 ` 24 + -IsError ` 25 + -Title $TheFunny ` 26 + -Body { 29 27 div -Class "n-box text-center gap-2" { 30 28 h1 { 31 29 span -Class 'text-6xl' { $Data.Status.Code } ··· 35 33 36 34 hr 37 35 38 - "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!" 36 + $TheFunny 39 37 40 38 hr 41 39 42 40 a -Href '/' -Class 'clickable p-4' { 'go home......' } 43 - 44 - if (Get-Item "$Root$Path/.." -ErrorAction Ignore) { 45 - a -Href '..' -Class 'clickable p-4' { '... or go up' } 46 - } 47 41 } 48 - 49 - div -Class "n-box" { 50 - pre { code { $Data | Out-String -Width 1024 } } 51 - } 52 - 53 - _footer 54 42 } 55 - }
+96 -13
functions.ps1
··· 222 222 {$size -eq '-'} { return '0 B' } 223 223 default { return "$size B" } 224 224 } 225 - } 225 + } 226 226 227 - function _check_joinedpath_is_based ($Root, $JoinedPath) { 228 - $Resolved = Get-Item -LiteralPath $JoinedPath -ErrorAction Ignore 229 - 230 - return ($null -ne $Resolved -and $Resolved.FullName.StartsWith($Root)) 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 + ) 231 239 } 232 240 233 241 function _script_hx_hs_jq { ··· 235 243 script -Src /_hs_tailwind.js 236 244 script -Src /htmx.js 237 245 script -Src /jquery.js 238 - meta -Name htmx-config -Content (@{ scrollIntoViewOnBoost=$false; defaultHideShowStrategy='twDisplay' }|ConvertTo-Json -Compress) 246 + meta -Name htmx-config -Content (@{ scrollIntoViewOnBoost=$false; defaultHideShowStrategy='twDisplay' } | ConvertTo-Json -Compress) 239 247 } 240 248 241 249 function _footer { ··· 254 262 } 255 263 } 256 264 257 - $FileTypes = @{ 265 + $MapExtensionToAbbr = @{ 258 266 '.avc' = ( _icon 'Video file' '🎞️' ) 259 267 '.flv' = ( _icon 'Video file' '🎞️' ) 260 268 '.mts' = ( _icon 'Video file' '🎞️' ) ··· 414 422 if (-not (_is_file $FSO)) { 415 423 _icon 'Directory' '📁' 416 424 } else { 417 - $Icon = $FileTypes[$FSO.Extension] 425 + $Icon = $MapExtensionToAbbr[$FSO.Extension] 426 + 418 427 if ([String]::IsNullOrEmpty($Icon)) { 419 428 if (-not $IsWindows -and (_can_r_x $FSO)) { 420 - _icon 'Generic executable (+x)' '🔳' 429 + return _icon 'Generic executable (+x)' '🔳' 430 + } else { 431 + return _icon 'File' '📄' 421 432 } 422 - 423 - _icon 'File' '📄' 424 433 } 425 434 return $Icon 426 435 } ··· 505 514 </td> 506 515 <td 507 516 class=file-size 508 - data-order=$($IsFile ? $Size : 0) 517 + data-order=$($IsFile ? $Size : -1) 509 518 >$( $IsFile ? (_format_size ([uint64] $Size)) : '' )</td> 510 519 <td 511 520 class=file-date ··· 525 534 return ( 526 535 tr { 527 536 td 528 - 537 + 529 538 td { _icon 'Error occured while generating this row' '⚠️' } 530 539 531 540 td -Class "file-name" -Attributes @{ "data-order" = $Item.Name } { ··· 550 559 551 560 return ($Parent ? '/' : $Parent) 552 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 + }
+42 -43
public/style.css
··· 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-300: oklch(80.8% 0.114 19.571); 13 + --color-red-400: oklch(70.4% 0.191 22.216); 14 14 --color-red-500: oklch(63.7% 0.237 25.331); 15 15 --color-green-100: oklch(96.2% 0.044 156.743); 16 16 --color-green-200: oklch(92.5% 0.084 155.995); 17 17 --color-green-300: oklch(87.1% 0.15 154.449); 18 18 --color-sky-800: oklch(44.3% 0.11 240.79); 19 19 --color-sky-950: oklch(29.3% 0.066 243.157); 20 + --color-blue-400: oklch(70.7% 0.165 254.624); 20 21 --color-blue-600: oklch(54.6% 0.245 262.881); 21 22 --color-gray-100: oklch(96.7% 0.003 264.542); 22 23 --color-gray-200: oklch(92.8% 0.006 264.531); ··· 206 207 .my-auto { 207 208 margin-block: auto; 208 209 } 209 - .ms-\[0\.5ch\] { 210 - margin-inline-start: 0.5ch; 211 - } 212 - .me-\[-1ch\] { 213 - margin-inline-end: -1ch; 214 - } 215 210 .flex { 216 211 display: flex; 217 212 } ··· 221 216 .table { 222 217 display: table; 223 218 } 224 - .h-fit { 225 - height: fit-content; 226 - } 227 219 .w-full { 228 220 width: 100%; 229 - } 230 - .max-w-64 { 231 - max-width: calc(var(--spacing) * 64); 232 - } 233 - .min-w-64 { 234 - min-width: calc(var(--spacing) * 64); 235 221 } 236 222 .cursor-default\! { 237 223 cursor: default !important; 238 224 } 239 - .cursor-pointer { 240 - cursor: pointer; 241 - } 242 225 .flex-col { 243 226 flex-direction: column; 244 227 } ··· 257 240 .border-black { 258 241 border-color: var(--color-black); 259 242 } 260 - .from-red-300\! { 261 - --tw-gradient-from: var(--color-red-300) !important; 243 + .from-red-400\! { 244 + --tw-gradient-from: var(--color-red-400) !important; 262 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; 263 246 } 264 247 .to-red-500\! { ··· 305 288 .text-stone-500 { 306 289 color: var(--color-stone-500); 307 290 } 308 - .text-transparent { 309 - color: transparent; 310 - } 311 291 .underline { 312 292 text-decoration-line: underline; 313 293 } 314 - .select-none { 315 - -webkit-user-select: none; 316 - user-select: none; 294 + .filter { 295 + 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,); 317 296 } 318 297 } 319 298 @font-face { ··· 3101 3080 --tw-duration: 100ms; 3102 3081 transition-duration: 100ms; 3103 3082 } 3083 + a:hover:not(td > a) { 3084 + background-color: color-mix(in srgb, oklch(70.7% 0.165 254.624) 15%, transparent); 3085 + @supports (color: color-mix(in lab, red, red)) { 3086 + & { 3087 + background-color: color-mix(in oklab, var(--color-blue-400) 15%, transparent); 3088 + } 3089 + } 3090 + } 3104 3091 hr { 3105 3092 margin-block: calc(var(--spacing) * 4); 3106 3093 color: var(--color-zinc-400); 3094 + } 3095 + #main { 3096 + height: fit-content; 3107 3097 } 3108 3098 .n-box { 3109 3099 margin-inline: auto; ··· 3320 3310 justify-content: stretch; 3321 3311 gap: 2ch; 3322 3312 white-space: nowrap; 3313 + #go-back { 3314 + cursor: pointer; 3315 + } 3316 + #search-label { 3317 + margin-block: auto; 3318 + margin-inline-start: 0.5ch; 3319 + margin-inline-end: -1ch; 3320 + cursor: default !important; 3321 + } 3323 3322 } 3324 3323 #rx-label { 3325 3324 margin-block: auto; ··· 3504 3503 inherits: false; 3505 3504 initial-value: 100%; 3506 3505 } 3507 - @property --tw-leading { 3508 - syntax: "*"; 3509 - inherits: false; 3510 - } 3511 - @property --tw-duration { 3512 - syntax: "*"; 3513 - inherits: false; 3514 - } 3515 - @property --tw-border-style { 3516 - syntax: "*"; 3517 - inherits: false; 3518 - initial-value: solid; 3519 - } 3520 3506 @property --tw-blur { 3521 3507 syntax: "*"; 3522 3508 inherits: false; ··· 3570 3556 syntax: "*"; 3571 3557 inherits: false; 3572 3558 } 3559 + @property --tw-leading { 3560 + syntax: "*"; 3561 + inherits: false; 3562 + } 3563 + @property --tw-duration { 3564 + syntax: "*"; 3565 + inherits: false; 3566 + } 3567 + @property --tw-border-style { 3568 + syntax: "*"; 3569 + inherits: false; 3570 + initial-value: solid; 3571 + } 3573 3572 @property --tw-font-weight { 3574 3573 syntax: "*"; 3575 3574 inherits: false; ··· 3591 3590 --tw-gradient-from-position: 0%; 3592 3591 --tw-gradient-via-position: 50%; 3593 3592 --tw-gradient-to-position: 100%; 3594 - --tw-leading: initial; 3595 - --tw-duration: initial; 3596 - --tw-border-style: solid; 3597 3593 --tw-blur: initial; 3598 3594 --tw-brightness: initial; 3599 3595 --tw-contrast: initial; ··· 3607 3603 --tw-drop-shadow-color: initial; 3608 3604 --tw-drop-shadow-alpha: 100%; 3609 3605 --tw-drop-shadow-size: initial; 3606 + --tw-leading: initial; 3607 + --tw-duration: initial; 3608 + --tw-border-style: solid; 3610 3609 --tw-font-weight: initial; 3611 3610 } 3612 3611 }
+5 -3
server.psd1
··· 16 16 #> 17 17 18 18 @{ 19 - Utatane = @{ 20 - Root = '/media/share' 21 - } 19 + Utatane = ( Microsoft.Powershell.Utility\Import-PowerShellDataFile ./Utatane.psd1 ) 22 20 23 21 Server = @{ 24 22 AllowedActions = @{ ··· 30 28 Web = @{ 31 29 Compression = @{ 32 30 Enable = $true 31 + } 32 + 33 + ErrorPages = @{ 34 + StrictContentTyping = $true 33 35 } 34 36 35 37 Static = @{
+29
templates/frame.ps1
··· 1 + param( 2 + [Switch] $IsError, 3 + [ScriptBlock] $Head, 4 + [String]$Title, 5 + 6 + [ScriptBlock] $Body 7 + ) 8 + 9 + doctype 10 + 11 + html { 12 + head { 13 + link -Rel stylesheet -Href '/style.css' 14 + title $Title 15 + 16 + _script_hx_hs_jq 17 + 18 + if ($Head) { 19 + $Head.Invoke() 20 + } 21 + } 22 + 23 + body -Class "flex flex-col $($IsError ? 'from-red-400! to-red-500!' : $null)" { 24 + if ($Body) { 25 + $Body.Invoke() 26 + } 27 + _footer 28 + } 29 + }
+26
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 + 'Read this source code in the footer link!' 25 + } 26 + }
+12 -14
views/api/files.html.ps1
··· 30 30 31 31 $ForceThreadedRenderer = $WebEvent.Query.threaded_renderer -eq 'true' ? $true : $false 32 32 33 - $Listing = gci -Lit $JoinedPath 33 + $Listing = Get-ChildItem -Lit $JoinedPath | ? { -not (_check_path_on_blacklist $_) } 34 34 $MaxSize = 100 35 35 $BatchSize = 1000 36 36 $IsFirst = $true 37 37 38 38 <# ### ### ### #> 39 39 40 - if (-not (_is_readable (gi $JoinedPath))) { 40 + if (-not (_is_readable (Get-Item $JoinedPath))) { 41 41 return ( 42 42 tr { 43 43 td ··· 50 50 } 51 51 52 52 if ($Listing.Count -gt ($MaxSize * 2)) { 53 - $x = [System.Diagnostics.Stopwatch]::new() 54 - $x.Start(); 53 + $Timer = [System.Diagnostics.Stopwatch]::new() 54 + $Timer.Start(); 55 55 56 56 # Optimization for massive file listings: split them up & do batch processing 57 57 $BatchList = [System.Collections.Generic.List[Array]]::new() ··· 70 70 71 71 $Batch = $_ 72 72 73 - "running batch $ID (size:$($Batch.Count))"|Write-Host 73 + _info "running batch $ID (size:$($Batch.Count))" 74 74 75 75 $Batch | % { 76 76 $Entry = $_ ··· 80 80 }) 81 81 } 82 82 83 - "Finished batch $ID (size:$($Batch.Count))"|Write-Host 83 + _info "Finished batch $ID (size:$($Batch.Count))" 84 84 } 85 85 86 86 $Rows.ToArray() | Sort-Object -Prop ` ··· 89 89 @{ Expression={ $_.Entry.Name }; Descending=$False} 90 90 | % Row 91 91 92 - $x.stop() 93 - _warn "table took $x" 92 + $Timer.stop() 93 + _warn "Fast renderer took $Timer for $($Listing.Count) items (in $($Batch.Count) batches)" 94 94 } else { 95 - $x = [System.Diagnostics.Stopwatch]::new() 96 - $x.Start(); 97 - 98 - "Using basic renderer with $($Listing.Count) items"|Write-Host 95 + $Timer = [System.Diagnostics.Stopwatch]::new() 96 + $Timer.Start(); 99 97 100 98 $ListingSorted = $Listing | Sort-Object -Prop ` 101 99 @{ Expression={ _is_file $_ }; Descending=$False}, ` ··· 113 111 } 114 112 } 115 113 116 - $x.stop() 117 - _warn "table took $x" 114 + $Timer.Stop() 115 + _info "Basic render took $Timer for $($Listing.Count) items" 118 116 } 119 117 120 118 function _sorter {
+11 -5
views/api/files.json.ps1
··· 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 - 18 17 param ( 19 18 $Data 20 19 ) 21 20 22 - $JoinedPath = Join-Path $Data.Root $Data.Path 21 + $Root = $Data.Root 22 + $CurrentPath = ($WebEvent.Query.Path -eq '/') ? "" : $WebEvent.Query.Path 23 + $JoinedPath = $Data.JoinedPath 23 24 24 - gci $JoinedPath | % { 25 + Get-ChildItem -Lit $JoinedPath | ? { -not (_check_path_on_blacklist $_) } | % { 26 + # bug ???? 27 + if ($_ -isnot [IO.FileSystemInfo]) { 28 + return 29 + } 30 + 25 31 $is_file = _is_file $_ 26 32 27 33 @{ 34 + type = $is_file ? 'file' : 'directory' 28 35 name = $_.Name 29 36 size = $is_file ? $_.size : $null # check if works for symlinks!! 30 37 last_modified = [uint64](Get-Date $_.LastWriteTime -UFormat '%s' -AsUTC) 31 - type = $is_file ? 'file' : 'directory' 32 38 } 33 - } | ConvertTo-Json -Depth 1 -Compress 39 + }
-18
views/api/list.html.ps1
··· 1 - $Root = $Data.Root 2 - $CurrentPath = ($WebEvent.Query.Path -eq '/') ? "" : $WebEvent.Query.Path 3 - $JoinedPath = $Data.JoinedPath 4 - 5 - <# ### ### ### #> 6 - 7 - ul ` 8 - -Id 'tree-navigator' ` 9 - -Class 'flex flex-col m-box min-w-64 max-w-64 font-mono h-fit' ` 10 - -HxPreserve ` 11 - { 12 - gci -Lit $JoinedPath -Dir | % { 13 - li -Class 'tree-row' { 14 - span -Class 'dir-indicator' { '+' } 15 - a -Href '#' -_hs "on click set my *background-color to 'red'" { $_.Name } 16 - } 17 - } 18 - }
+9
views/index.json.ps1
··· 1 + $Root = $Data.Root 2 + $Path = $Data.Path 3 + $JoinedPath = Join-Path $Root $Path 4 + 5 + <# ### ### ### #> 6 + 7 + @{ 8 + Message = "Query /api/files?path=$(_encode $Path) with header Accept: application/json" 9 + }
+6 -17
views/index.ps1 views/index.html.ps1
··· 21 21 22 22 <# ### ### ### #> 23 23 24 - doctype 25 - 26 - html { 27 - head { 28 - link -Rel stylesheet -Href '/style.css' 29 - title "File index for $Path" 30 - 31 - _script_hx_hs_jq 32 - 24 + . ./templates/frame.ps1 ` 25 + -Title "Index: $Path" ` 26 + -Head { 33 27 script -Type text/hyperscript { @" 34 28 def get_dir(x) 35 29 if not x ··· 61 55 end 62 56 "@ 63 57 } 64 - } 65 - 66 - body -Class 'flex flex-col' { 58 + } ` 59 + -Body { 67 60 header -Class 'n-box' { 68 61 div -Id 'breadcrumbs' { 69 62 a -Href '/' -Class 'clickable' -HxBoost true { '/' } ··· 247 240 } 248 241 } 249 242 250 - tbody -Id "filetable-body" -Class 'htmx-indicator' { 243 + tbody -Id 'filetable-body' -Class 'htmx-indicator' { 251 244 # pregen rows to prevent massive layout shift 252 245 gci -lit $JoinedPath | % { 253 246 tr { ··· 261 254 } 262 255 } 263 256 } 264 - 265 257 } 266 - 267 - _footer 268 258 } 269 - }