Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File Browsing feature for Pode Static Route #1266

Merged
merged 17 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading