nginx-interactive-index: Init

This commit is contained in:
Simon Bruder 2021-02-15 12:09:55 +01:00
parent ceff40f84d
commit e0ef586e5e
Signed by: simon
GPG key ID: 8D3C82F9F309F8EC
4 changed files with 190 additions and 0 deletions

View file

@ -46,6 +46,7 @@ in
./mailserver.nix ./mailserver.nix
./media-proxy.nix ./media-proxy.nix
./network-manager.nix ./network-manager.nix
./nginx-interactive-index
./office.nix ./office.nix
./prometheus/node_exporter.nix ./prometheus/node_exporter.nix
./pubkeys.nix ./pubkeys.nix

View file

@ -0,0 +1,69 @@
# This module implements an option with the same structure as the nginx module
# but does not extend the nginx module since that would cause infinite
# recursion.
{ config, lib, pkgs, ... }:
let
enabledLocations = lib.fold
(x: a: a ++ x)
[ ]
(lib.mapAttrsToList
(vhostName: vhostConfig: lib.mapAttrsToList
(locationName: locationConfig: [ vhostName locationName ])
(lib.filterAttrs
(_: location: location.enable)
vhostConfig.locations))
config.services.nginx-interactive-index.virtualHosts);
in
{
options.services.nginx-interactive-index.virtualHosts = with lib.types; lib.mkOption {
default = { };
type = attrsOf (submodule {
options = {
locations = lib.mkOption {
default = { };
type = attrsOf (submodule {
options = {
enable = lib.mkEnableOption "interactive directory index";
};
});
};
};
});
};
config.services.nginx.virtualHosts = lib.fold
(x: a: a // x)
{ }
(map
(path:
let
vhost = lib.elemAt path 0;
location = lib.elemAt path 1;
assetsPath = "${location}__nginx-interactive-index-assets__";
in
{
"${vhost}".locations = {
"${location}" = {
extraConfig = ''
autoindex on;
autoindex_exact_size on;
add_before_body ${assetsPath}/header.html;
'';
};
"${assetsPath}/" = {
alias = "${builtins.filterSource
(path: type: baseNameOf path != "default.nix")
./.}/";
};
"=${assetsPath}/header.html" = {
alias = pkgs.writeText "nginx-interactive-index-${location}-header.html" ''
<!DOCTYPE html>
<meta charset="utf-8">
<link rel="stylesheet" href="${assetsPath}/listing.css">
<script src="${assetsPath}/listing.js"></script>
'';
};
};
})
enabledLocations);
}

View file

@ -0,0 +1,44 @@
body, html {
background-color: #fdf6e3;
color: #657b83;
font-family: "TeX Gyre Heros", "Roboto", "Helvetica", "Arial", sans-serif;
}
tr:nth-child(even) {
background: #eee8d5;
}
th, td {
padding: 0.1em 0.5em;
}
th {
text-align: left;
font-weight: bold;
background: #eee8d5;
border-bottom: 1px solid #657b83;
}
a {
color: #586e75;
}
a:hover {
color: #073642;
}
table {
width: 100%;
}
#search-field {
width: 100%;
border: none;
margin-bottom: 15px;
background: #eee8d5;
color: inherit;
}
hr {
display: none;
}

View file

@ -0,0 +1,76 @@
document.addEventListener('DOMContentLoaded', () => {
function humanFileSize(bytes) {
const thresh = 1024
if(Math.abs(bytes) < thresh) {
return bytes + ' B'
}
const units = ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB']
var u = -1
do {
bytes /= thresh
++u
} while(Math.abs(bytes) >= thresh && u < units.length - 1)
return bytes.toFixed(1)+' '+units[u]
}
function textToA(line) {
let outerElement = document.createElement('div')
outerElement.innerHTML = line
return outerElement.getElementsByTagName('a')[0]
}
function parseLine(line) {
const href = textToA(line).href
const filename = href.substr(-1) === '/' ? decodeURIComponent(href.split('/').slice(-2, -1)[0]) : decodeURIComponent(href.split('/').pop())
const size = line.split(' ').pop()
return {
href: href,
filename: filename,
size: size
}
}
function processLine(line) {
meta = parseLine(line)
return `<tr><td><a href="${meta.href}">${meta.filename}</a></td><td>${meta.size === '-' ? '-' : humanFileSize(meta.size)}</td></tr>`
}
const collator = new Intl.Collator('kn', {numeric: true})
// transform plain text to table
document.querySelector('pre').outerHTML = '<table><tr><th>Name</th><th>Size</th></tr><tr><td><a href="..">..</a></td><td>-</td></tr>' + document.querySelector('pre').innerHTML
.split('\n')
.filter(line => line !== '')
.filter(line => line !== '<a href="../">../</a>')
.map(processLine)
.sort(collator.compare)
.join('\n') + '</table>'
let searchField = document.createElement('input')
searchField.id = 'search-field'
searchField.autofocus = true
document.querySelector('body').insertBefore(searchField, document.querySelector('table'))
const rows = Array.from(document.querySelectorAll('tr:not(:first-child)'))
document.querySelector('#search-field').addEventListener("input", e => {
const searchValue = e.target.value.toLowerCase()
rows.forEach(row => {
const file = row.querySelector('td:nth-child(1) a').innerText
if (!file.toLowerCase().includes(searchValue)) {
row.style.display = 'none'
} else {
row.style.display = 'table-row'
}
})
const visibleRows = rows.filter(row => row.style.display === 'table-row')
if (visibleRows.length === 1) {
const target = visibleRows[0].querySelector('td a').href
if (target.substr(-1) === '/') {
window.location = target
}
}
})
})