Skip to content

Commit

Permalink
Merge pull request #1266 from mdaneri/#1237
Browse files Browse the repository at this point in the history
File Browsing feature for Pode Static Route
  • Loading branch information
Badgerati authored Apr 4, 2024
2 parents 90d3602 + ec8eda1 commit c418442
Show file tree
Hide file tree
Showing 16 changed files with 978 additions and 155 deletions.
30 changes: 15 additions & 15 deletions docs/Tutorials/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,18 @@ A "path" like `Server.Ssl.Protocols` looks like the below in the file:
}
```

| Path | Description | Docs |
| --------------------- | ----------------------------------------------------------------------- | ------------------------------------------ |
| Server.Ssl.Protocols | Indicates the SSL Protocols that should be used | [link](../Certificates) |
| Server.Request | Defines request timeout and maximum body size | [link](../RequestLimits) |
| Server.AutoImport | Defines the AutoImport scoping rules for Modules, SnapIns and Functions | [link](../Scoping) |
| Server.Logging | Defines extra configuration for Logging, like masking sensitive data | [link](../Logging/Overview) |
| Server.Root | Overrides root path of the server | [link](../Misc/ServerRoot) |
| Server.Restart | Defines configuration for automatically restarting the server | [link](../Restarting/Types/AutoRestarting) |
| Server.FileMonitor | Defines configuration for restarting the server based on file updates | [link](../Restarting/Types/FileMonitoring) |
| Server.DefaultFolders | Define custom default folders | [link](../Routes/Utilities/StaticContent) |
| Web.TransferEncoding | Sets the Request TransferEncoding | [link](../Compression/Requests) |
| Web.Compression | Sets any compression to use on the Response | [link](../Compression/Responses) |
| Web.ContentType | Define expected Content Types for certain Routes | [link](../Routes/Utilities/ContentTypes) |
| Web.ErrorPages | Defines configuration for custom error pages | [link](../Routes/Utilities/ErrorPages) |
| Web.Static | Defines configuration for static content, such as caching | [link](../Routes/Utilities/StaticContent) |
| Path | Description | Docs |
| ------------------------------ | ----------------------------------------------------------------------- | ------------------------------------------ |
| Server.Ssl.Protocols | Indicates the SSL Protocols that should be used | [link](../Certificates) |
| Server.Request | Defines request timeout and maximum body size | [link](../RequestLimits) |
| Server.AutoImport | Defines the AutoImport scoping rules for Modules, SnapIns and Functions | [link](../Scoping) |
| Server.Logging | Defines extra configuration for Logging, like masking sensitive data | [link](../Logging/Overview) |
| Server.Root | Overrides root path of the server | [link](../Misc/ServerRoot) |
| Server.Restart | Defines configuration for automatically restarting the server | [link](../Restarting/Types/AutoRestarting) |
| Server.FileMonitor | Defines configuration for restarting the server based on file updates | [link](../Restarting/Types/FileMonitoring) |
| Web.Static.ValidateLast | Changes the way routes are processed. | [link](../Routes/Utilities/StaticContent) |
| Web.TransferEncoding | Sets the Request TransferEncoding | [link](../Compression/Requests) |
| Web.Compression | Sets any compression to use on the Response | [link](../Compression/Responses) |
| Web.ContentType | Define expected Content Types for certain Routes | [link](../Routes/Utilities/ContentTypes) |
| Web.ErrorPages | Defines configuration for custom error pages | [link](../Routes/Utilities/ErrorPages) |
| Web.Static | Defines configuration for static content, such as caching | [link](../Routes/Utilities/StaticContent) |
37 changes: 37 additions & 0 deletions docs/Tutorials/Routes/Utilities/StaticContent.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,40 @@ Start-PodeServer {
```

When a static route is set as downloadable, then `-Defaults` and caching are not used.

## File Browsing

This feature allows the use of a static route as an HTML file browser. If you set the `-FileBrowser` switch on the [`Add-PodeStaticRoute`] function, the route will show the folder content whenever it is invoked.

```powershell
Start-PodeServer -ScriptBlock {
Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http
Add-PodeStaticRoute -Path '/' -Source './content/assets' -FileBrowser
Add-PodeStaticRoute -Path '/download' -Source './content/newassets' -DownloadOnly -FileBrowser
}
```

When used with `-DownloadOnly`, the browser downloads any file selected instead of rendering. The folders are rendered and not downloaded.

## Static Routes order
By default, Static routes are processed before any other route.
There are situations where you want a main `GET` route has the priority to a static one.
For example, you have to hide or make some computation to a file or a folder before returning the result.

```powershell
Start-PodeServer -ScriptBlock {
Add-PodeRoute -Method Get -Path '/LICENSE.txt' -ScriptBlock {
$value = @'
Don't kidding me. Nobody will believe that you want to read this legalise nonsense.
I want to be kind; this is a summary of the content:
Nothing to report :D
'@
Write-PodeTextResponse -Value $value
}
Add-PodeStaticRoute -Path '/' -Source "./content" -FileBrowser
}
```

To change the default behavior, you can use the `Web.Static.ValidateLast` property in the `server.psd1` configuration file, setting the value to `$True.` This will ensure that any static route is evaluated after any other route.
58 changes: 58 additions & 0 deletions examples/FileBrowser/FileBrowser.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
$FileBrowserPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Path
$podePath = Split-Path -Parent -Path (Split-Path -Parent -Path $FileBrowserPath)
if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) {
Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop
}
else {
Import-Module -Name 'Pode'
}

$directoryPath = $podePath
# Start Pode server
Start-PodeServer -ScriptBlock {

Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http -Default

New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging
New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging

# setup basic auth (base64> username:password in header)
New-PodeAuthScheme -Basic -Realm 'Pode Static Page' | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock {
param($username, $password)

# here you'd check a real user storage, this is just for example
if ($username -eq 'morty' -and $password -eq 'pickle') {
return @{
User = @{
ID = 'M0R7Y302'
Name = 'Morty'
Type = 'Human'
}
}
}

return @{ Message = 'Invalid details supplied' }
}
Add-PodeRoute -Method Get -Path '/LICENSE.txt' -ScriptBlock {
$value = @'
Don't kidding me. Nobody will believe that you want to read this legalise nonsense.
I want to be kind; this is a summary of the content:
Nothing to report :D
'@
Write-PodeTextResponse -Value $value
}
Add-PodeStaticRouteGroup -FileBrowser -Routes {

Add-PodeStaticRoute -Path '/' -Source $using:directoryPath
Add-PodeStaticRoute -Path '/download' -Source $using:directoryPath -DownloadOnly
Add-PodeStaticRoute -Path '/nodownload' -Source $using:directoryPath
Add-PodeStaticRoute -Path '/any/*/test' -Source $using:directoryPath
Add-PodeStaticRoute -Path '/auth' -Source $using:directoryPath -Authentication 'Validate'
}
Add-PodeStaticRoute -Path '/nobrowsing' -Source $directoryPath

Add-PodeRoute -Method Get -Path '/attachment/*/test' -ScriptBlock {
Set-PodeResponseAttachment -Path 'ruler.png'
}
}
Binary file added examples/FileBrowser/public/ruler.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
221 changes: 221 additions & 0 deletions src/Misc/default-file-browsing.html.pode
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<html lang="en" ; style='background-color: #01b0c4' ;>

<head>
<title>File Browser:$($Data.Path)</title>
<meta charset='utf-8'>

<style>
body {
font-family: Helvetica,Arial,sans-serif;
/* Applies monospace font to <h2> */
}

/* Add CSS styles here for column widths */

.icon {
width: 10px;
/* Fixed width for the icon column */
text-align: right;
/* Centers the icon within the column */
vertical-align: middle;
/* Aligns the icon vertically in the middle of the cell */

}

.unixMode,
.mode {
width: 60px;
text-align: left;
}

.user {
width: 40px;
text-align: right;
}

.group {
width: 40px;
text-align: right;
}

.DateTime {
width: 200px;
text-align: right;
/* Align text to the right */
}

.size {
width: 80px;
max-width: 80px;
min-width: 80px;
text-align: right;
/* Align text to the right */
padding-right: 20px;
/* Adds space to the right inside the Size cell */
}

.name {
width: 200px;
text-align: left;
}

a {
color: blue;
/* Or any color that fits your design */
text-decoration: none;
/* Removes underline */
}

a:hover {
text-decoration: underline;
/* Optional: underline on hover for visual feedback */
}


table {
table-layout: fixed;
width: 100%;
font-family: 'Courier New', Courier, monospace;
/* Applies monospace font to the table */
/* border-collapse: collapse;
Removes the space between borders */
}

th {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: bold;
/* Makes the header text bold */
/* border-bottom: 1px solid #000; Adds a solid line under the header */
}

td {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

/* Style the container to match h3 styling */
#pathContainer {
font-size: 1.47em;
/* Standard size for h3 */
font-weight: bold;
/* Bold text for h3 */
margin-top: 20px;
/* Spacing for visual separation */
margin-bottom: 30px; /* Additional space after the container */
}

#pathContainer a,
#pathContainer span {
margin-right: -1px;
/* Reduced spacing between segments */
}

#pathContainer span.separator {
margin-right: 0;
/* Remove spacing after separator */
}
</style>

</head>
<script>
// Example JavaScript to toggle between Windows and Unix mode
document.addEventListener('DOMContentLoaded', function () {
var WindowsMode = $($Data.windowsMode);
var tableContent = '';
if (WindowsMode) {
tableContent += '<tr> <th class="mode">Mode</th> <th class="dateTime">CreationTime</th> <th class="dateTime">LastWriteTime</th> <th class="size">Size</th> <th class="icon"></th> <th class="name">Name</th> </tr> <tr> <th class="mode">----</th>' +
'<th class="dateTime">-------------</th> <th class="dateTime">-------------</th> <th class="size">----</th> <th class="icon"></th> <th class="name">----</th> </tr>';
} else {
tableContent += '<tr> <th class="unixMode">UnixMode</th> <th class="user">User</th> <th class="group">Group</th> <th class="dateTime">CreationTime</th> <th class="dateTime">LastWriteTime</th> <th class="size">Size</th> <th class="icon"></th> <th class="name">Name</th> </tr>' +
'<tr> <th class="unixMode">--------</th> <th class="user">----</th> <th class="group">-----</th> <th class="dateTime">-------------</th> <th class="dateTime">-------------</th> <th class="size">----</th> <th class="icon"></th> <th class="name">----</th> </tr>';
}

// Insert the table content into the table body
document.getElementById('tableBody').innerHTML = tableContent;
});

function visualizePathSegments() {
const basePath = '$($Data.Path)'; // Your given URL path
const segments = basePath.split('/').filter(Boolean); // Split by '/' and remove any empty segments
const pathContainer = document.getElementById('pathContainer');
pathContainer.innerHTML = ''; // Clear previous content

// Create the Directory label as a span to keep it inline
const directoryLabel = document.createElement('span');
directoryLabel.textContent = 'Folder Path: ';
pathContainer.appendChild(directoryLabel); // Append the label to the container

// Make the root a clickable link and add a space after it
const rootPath = '$($Data.RootPath)/'; // Adjusted for correct root path
const rootLinkSpan = document.createElement('span'); // Create a span to hold the root link and the space
const rootLink = document.createElement('a');
rootLink.href = rootPath;
rootLink.textContent = '/'; // Display text for root
rootLinkSpan.appendChild(rootLink);
rootLinkSpan.innerHTML += ' '; // Add a space after the root link
pathContainer.appendChild(rootLinkSpan);

segments.forEach((segment, index) => {
// Add a separator with spaces around it for better readability
if (index > 0) {
const separator = document.createElement('span');
separator.innerHTML = ' / '; // Added spaces around the separator
separator.classList.add('separator'); // Use for specific styling
pathContainer.appendChild(separator);
} else {
// For consistency, add a space before the first segment if there's no separator needed
const initialSpace = document.createElement('span');
initialSpace.innerHTML = ' ';
pathContainer.appendChild(initialSpace);
}

let cumulativePath = rootPath + segments.slice(0, index + 1).join('/');

if (index < segments.length - 1) {
// Create a clickable link for this segment
const a = document.createElement('a');
a.href = cumulativePath; // Here, the href is the cumulative path
a.textContent = segment; // Set text to segment
pathContainer.appendChild(a);
pathContainer.innerHTML += ' '; // Add a space after the segment link
} else {
// For the last segment, make it a label instead of a clickable link
const lastSegmentLabel = document.createElement('span');
lastSegmentLabel.textContent = segment; // Set text to the last segment
pathContainer.appendChild(lastSegmentLabel);
}
});
}

// Automatically visualize the path when the page loads
window.onload = visualizePathSegments;


</script>

<body style='color: white; '>

<div>
<div id="pathContainer"> </div>
<div></div>
<table>
<tbody id="tableBody">
</tbody>
$($Data.fileContent)
</table>
<p style='text-align: center;
width: 100%;
font-size: 0.9em;
position: fixed;
bottom: 0px;'>
🧡 Powered by <a href='https://badgerati.github.io/Pode/'
style='color: wheat; text-decoration: none;'>Pode</a>
</p>
</div>

</body>

</html>
1 change: 1 addition & 0 deletions src/Pode.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
'Write-PodeJsonResponse',
'Write-PodeXmlResponse',
'Write-PodeViewResponse',
'Write-PodeDirectoryResponse',
'Set-PodeResponseStatus',
'Move-PodeResponseUrl',
'Write-PodeTcpClient',
Expand Down
1 change: 1 addition & 0 deletions src/Private/Context.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,7 @@ function Set-PodeWebConfiguration {
Include = (Convert-PodePathPatternsToRegex -Paths @($Configuration.Static.Cache.Include) -NotSlashes)
Exclude = (Convert-PodePathPatternsToRegex -Paths @($Configuration.Static.Cache.Exclude) -NotSlashes)
}
ValidateLast = [bool]$Configuration.Static.ValidateLast
}
ErrorPages = @{
ShowExceptions = [bool]$Configuration.ErrorPages.ShowExceptions
Expand Down
Loading

0 comments on commit c418442

Please sign in to comment.