diff --git a/modules/default.nix b/modules/default.nix index c5d16cb..de3dfec 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -46,6 +46,7 @@ in ./mailserver.nix ./media-proxy.nix ./network-manager.nix + ./nginx-interactive-index ./office.nix ./prometheus/node_exporter.nix ./pubkeys.nix diff --git a/modules/nginx-interactive-index/default.nix b/modules/nginx-interactive-index/default.nix new file mode 100644 index 0000000..a93bc43 --- /dev/null +++ b/modules/nginx-interactive-index/default.nix @@ -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" '' + + + + + ''; + }; + }; + }) + enabledLocations); +} diff --git a/modules/nginx-interactive-index/listing.css b/modules/nginx-interactive-index/listing.css new file mode 100644 index 0000000..ce13f98 --- /dev/null +++ b/modules/nginx-interactive-index/listing.css @@ -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; +} diff --git a/modules/nginx-interactive-index/listing.js b/modules/nginx-interactive-index/listing.js new file mode 100644 index 0000000..c7841de --- /dev/null +++ b/modules/nginx-interactive-index/listing.js @@ -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 `${meta.filename}${meta.size === '-' ? '-' : humanFileSize(meta.size)}` + } + + const collator = new Intl.Collator('kn', {numeric: true}) + + // transform plain text to table + document.querySelector('pre').outerHTML = '' + document.querySelector('pre').innerHTML + .split('\n') + .filter(line => line !== '') + .filter(line => line !== '../') + .map(processLine) + .sort(collator.compare) + .join('\n') + '
NameSize
..-
' + + 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 + } + } + }) +})