This commit is contained in:
Mike Phares 2022-11-07 15:07:56 -07:00
commit 8229307f43
14 changed files with 10817 additions and 0 deletions

10
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"cSpell.words": [
"Phares"
],
"cSpell.enabled": true,
"files.exclude": {
"**/.git": false
},
"coverage-gutters.coverageBaseDir": "./.vscode/ReportGenerator/Cobertura/*"
}

24
LICENSE Normal file
View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

72
README.md Normal file
View File

@ -0,0 +1,72 @@
# [NGINdeX.io](#)
[NGINdeX.io](https://github.com/EthraZa/NGINdeX.io) is [NGINX](https://www.nginx.com/) [autoindex](https://nginx.org/en/docs/http/ngx_http_autoindex_module.html) redux.
A pretty but simple autoindex for Nginx without the fancy index module.
It will read up the content from a NGINX folder and create a pretty HTML5 table to browse it
with column sort and filter.
It uses a minimal CSS library and [vanilla JavaScript](http://vanilla-js.com/) for resources wise performance.
# Install
Downlod header.html and footer.html into a folder in your NGINX webserver
and copy the nginx.example.conf setup to your NGINX config file.
# Usage
It has no configs, except a couple of params that can be passed to the URL.
**Color Theme**
- t = theme_name
ie: https://domain.tld/folder?t=maroon
**Background image**
- b = Image_URL
ie: https://domain.tld/folder?b=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1569235186275-626cb53b83ce%3Fixlib%3Drb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1352&q=80&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1352&q=80
# CSS libraries and themes
It is tested and works with the followings CSS libraries:
- [Default style sheet for HTML 5](https://html.spec.whatwg.org/multipage/rendering.html#phrasing-content-3) - No CSS library at all
- [Milligram.io](https://milligram.io/) + [Normalize](https://necolas.github.io/normalize.css/) - A minimalist CSS framework **(Default)**
- [Min](https://mincss.com/) - The world's smallest CSS framework
- [Normalize](https://necolas.github.io/normalize.css/) - A modern, HTML5-ready alternative to CSS resets
- [Picnic CSS](https://picnicss.com/) - Lightweight and beautiful library
- [sscaffold](https://sscaffold-css.com/) - Lightweight css for people who build things
- [Color themes](https://cdn.jsdelivr.net/npm/milligram-themes@0.0.2/) - Milligram-themes (works with all CSS libraries above)
\* You can uncomment any of then at the top of header.html (\<head\>).
# How it works
1. The nginx autoindex module delivers the folder content in JSON format (autoindex_format json) with header.html at the beginning and footer.html at the end of the output
2. The downloaded JSON will end up inside a hidden DIV as text, without any AJAX call (add_header Content-Type text/html)
3. When the download is done, the JSON is read and parsed by JavaScript
4. The table with the server folder content is then build and the loading CSS animation is closed
# Screenshots
| <img src="https://raw.githubusercontent.com/EthraZa/NGINdeX.io/main/img/milligram.png" title="Milligram" width="320"> |
---
| <img src="https://raw.githubusercontent.com/EthraZa/NGINdeX.io/main/img/min.png" title="Min" width="320"> |
---
| <img src="https://raw.githubusercontent.com/EthraZa/NGINdeX.io/main/img/picnic.png" title="Picnic" width="320"> |
---
| <img src="https://raw.githubusercontent.com/EthraZa/NGINdeX.io/main/img/normalize.png" title="Normalize" width="320"> |
---
| <img src="https://raw.githubusercontent.com/EthraZa/NGINdeX.io/main/img/milligram_bg_maroon.png" title="Milligram Maroon with background" width="320"> |
---
| <img src="https://raw.githubusercontent.com/EthraZa/NGINdeX.io/main/img/milligram_bg_dark.png" title="Milligram Dark with background" width="320"> |
---
<br><br>
**NGINdeX.io is based on**
- [StrapIndex](https://github.com/EthraZa/NGINdeX.io/tree/nginx-autoindex) - Bootstrap version that is based on
- [ccarney16/nginx-autoindex](https://github.com/ccarney16/nginx-autoindex) - A fancier autoindex for nginx (without the fancy index module!)
---
<br><br><br>
# Quote
> "People will not look forward to posterity, who never look backward to their ancestors."
>
> ― Edmund Burke, Reflections on the Revolution in France

10356
dev/index.html Normal file

File diff suppressed because it is too large Load Diff

4
footer.html Normal file
View File

@ -0,0 +1,4 @@
</div>
</footer>
</body>
</html>

335
header.html Normal file
View File

@ -0,0 +1,335 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="description" content="NGINdeX.io is NGINX autoindex redux.">
<title>NGINdeX.io</title>
<link rel="shortcut icon" href="https://fonts.gstatic.com/s/i/materialicons/drive_file_move/v17/24px.svg" crossorigin="anonymous">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" crossorigin="anonymous">
<!-- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic" crossorigin="anonymous"> -->
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css" as="style" crossorigin="anonymous">
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.css" as="style" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.css" crossorigin="anonymous">
<!-- <link rel="preload" href="https://cdn.jsdelivr.net/npm/picnic" as="style" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/picnic" crossorigin="anonymous"> -->
<!-- <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/min/1.5.0/entireframework.min.css" as="style" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/min/1.5.0/entireframework.min.css" crossorigin="anonymous"> -->
<!-- <link rel="preload" href="https://cdn.jsdelivr.net/npm/sscaffold-css@0.1.1/sscaffold.min.css" integrity="sha256-tNrNp6fPTVnhpywTjgNV4jCx6W9d1wuALpwVmAQEYcs=" as="style" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sscaffold-css@0.1.1/sscaffold.min.css" integrity="sha256-tNrNp6fPTVnhpywTjgNV4jCx6W9d1wuALpwVmAQEYcs=" crossorigin="anonymous"> -->
<link rel="stylesheet" media="bogus">
<script>
window.$ = document.querySelector.bind(document);
window.$$ = document.querySelectorAll.bind(document);
Node.prototype.on = window.on = function (name, fn) {
this.addEventListener(name, fn);
}
NodeList.prototype.__proto__ = Array.prototype;
NodeList.prototype.on = NodeList.prototype.addEventListener = function (name, fn) {
this.forEach(function (elem, i) {
elem.on(name, fn);
});
}
/**
* Format bytes as human-readable text.
* @param bytes Number of bytes.
* @param si True to use metric (SI) units, aka powers of 1000. False to use binary (IEC), aka powers of 1024.
* @param dp Number of decimal places to display.
* @return Formatted string.
*/
function humanFileSize(bytes, si=false, dp=1) {
const thresh = si ? 1000 : 1024;
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
const units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
let u = -1;
const r = 10**dp;
do {
bytes /= thresh;
++u;
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
return bytes.toFixed(dp) + ' ' + units[u];
}
function setSortable(c = 'th') {
const getCellValue = function(tr, idx) {
return tr.children[idx].innerText || tr.children[idx].textContent;
}
const comparer = function(idx, asc) {
return function(a, b) {
return (function (v1, v2) {
return v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2)
})(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));
}
}
$$(c).forEach(function(th) {
th.addEventListener('click', (function(e) {
let i = (e.target.tagName == 'SPAN'? e.target : e.target.querySelector('span'));
const table = th.closest('table');
const tbody = table.querySelector('tbody');
if (table) {
const is = table.querySelectorAll('th span');
if (is) {
is.forEach(s => s.style.transform = 'scaleY(1)');
is.forEach(s => s.textContent = 'filter_list');
}
if (i) {
i.textContent = 'sort';
i.style.transform = this.asc? 'scaleY(-1)' : 'scaleY(1)';
}
Array.from(tbody.querySelectorAll('tr'))
.sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
.forEach(tr => tbody.appendChild(tr));
}}
))
});
}
function filterTable(v = '', l = 0, c = 'table tbody tr', e = 'td') {
const tr = $$(c);
let cc = 0;
v = v.toLowerCase();
if (l && v && v.length < l) { return; }
while (cc < tr.length) {
let td = tr[cc].getElementsByTagName(e),
dpl = 'none';
ccc = 0;
while (ccc < td.length) {
let txt = (td[ccc].textContent || td[ccc].innerText).toLowerCase();
if (txt.indexOf(v) > -1) {
dpl = '';
}
++ccc;
}
tr[cc].style.display = dpl;
++cc;
}
}
function getData(c = '#idx-json') {
const cdom = $(c);
if (cdom.textContent.indexOf('[') >= 0) {
return JSON.parse(cdom.textContent);
}
}
function setTheme(v = '') {
$('#idx-theme').href = v? `https://cdn.jsdelivr.net/npm/milligram-themes@0.0.2/src/${v}.min.css` : v;
}
function getHref(u = ''){
return u + window.location.search;
}
function setEnv(s = '#idx-filter', t = '#idx-stheme') {
const pathname = window.location.pathname,
prm = new URLSearchParams(window.location.search);
document.removeEventListener('DOMContentLoaded', main);
if (prm.has('b')) {
$('section').style.backgroundImage = `url("${decodeURIComponent(prm.get('b'))}")`;
}
if (prm.has('t')) {
setTheme($(t).value = prm.get('t'));
}
$(t).addEventListener('change', function(e) {
setTheme(e.target.value);
prm.delete('t');
prm.append('t', e.target.value);
history.replaceState('', '', `${window.location.pathname}?${prm.toString()}`);
});
$(s).addEventListener('keyup', function(e) { filterTable(e.target.value, 3); });
setSortable();
$('#idx-path').innerHTML = '<small>' +
pathname.split('/').reduce(function(a, v, i, o) {
return a += ` / <a href="#" onclick="window.open(getHref('/${o.slice(1, i+1).join('/')}'),'_self')">${v}</a> `;
}); +
'</small>'
}
function setData(a = [], n = [0,0,0,0], p = 15000, m = 1, c = 'table tbody', f = 'footer') {
const pathname = window.location.pathname;
let tblHtm = '';
while (n[0] < a.length) {
let entry = a[n[0]];
mtime = new Date(entry.mtime),
entry.mtime = `${mtime.toLocaleDateString()} ${mtime.toLocaleTimeString()}`,
url = `${pathname}${entry.name}`;
if (entry.type == 'directory') {
++n[1];
tblHtm += `<tr>
<td><span class="material-icons" title="Folder">folder</span><span style="display:none">${entry.type}</span></td>
<td><a href="#" onclick="window.open(getHref('${url}'),'_self')">${entry.name}</a></td>
<td>${entry.mtime}</td>
<td></td>
</tr>`;
} else {
++n[2];
n[3] += entry.size;
tblHtm += `<tr>
<td><span class="material-icons" title="File">insert_drive_file</span><span style="display:none">${entry.type}</span></td>
<td><a href="${url}">${entry.name}</a>
<span style="float:right">
<a download="${entry.name}" href="${url}">
<span class="material-icons" title="Save">download</span>
</a>
<a href="${url}" target="_blank">
<span class="material-icons" title="Open">open_in_browser</span>
</a>
</span>
</td>
<td>${entry.mtime}</td>
<td>${humanFileSize(entry.size, true, 2)}</td>
</tr>`;
}
if (n[0] && n[0] != a.length-1 && n[0]%p === 0) {
$(c).innerHTML += tblHtm;
if (a.length >= p && n[0] == p) { $('.load').remove(); }
++n[0];
setTimeout(function() {
setData(a,n);
}, m);
return;
}
++n[0];
if (a.length < p && n[0] == a.length) { $('.load').remove(); }
}
$(c).innerHTML += tblHtm;
if (n[0] == a.length) {
$(f).innerHTML = `<span class="material-icons">folder_open</span><span>${n[1]}</span>
<span class="material-icons">description</span><span>${n[2]}</span>
<span class="material-icons">account_tree</span><span>${(n[1]+n[2])}<strong> : </strong> &nbsp;&nbsp;&nbsp;${humanFileSize(n[3], true, 3)}</span>`;
a = null;
}
}
function main() {
const jsn = getData();
setEnv();
if (jsn) {
setData(jsn);
} else if (window.location.pathname.endsWith('.html')) {
alert('Is NGINX autoindexing?');
}
}
document.addEventListener('DOMContentLoaded', main);
</script>
<style>
html{height: 100%; margin: 0}
body{text-align: left; overflow: hidden; height: 100%; display: flex; flex-direction: column; flex-wrap: nowrap; flex-direction: column;}
header{flex-shrink: 0;}
header ul{border-bottom:solid 1px #ccc; margin: 7px; display: flex; align-items: stretch; justify-content: space-between;}
header li{display:inline; margin:0 20px; white-space: nowrap;}
main{overflow: auto scroll; flex-grow: 1; padding: 8px 10px; display: flex; justify-content: center; align-items: flex-start;}
footer{flex-shrink: 0; text-align: right; margin:0; border-top: solid 1px #ccc; z-index: 4;}
table{width: calc(100vw / 1.618);}
th{position: sticky; top: 0px; z-index: 2; white-space: nowrap; cursor: pointer;}
th span.material-icons{vertical-align: top; padding: 0 0 0 1em;}
th:first-child{padding: unset;}
td:first-child{width: 32px;}
td:nth-child(n+3){width: 1%; white-space: nowrap;}
tr{transition: all 0.2s ease 0s;}
tr:hover{box-shadow: 0px 5px 40px -10px rgba(0,0,0,0.5);}
span.material-icons + span{vertical-align: middle; padding: 0 2em 0 1em;}
section{width: 100%; height: 100%; position: fixed; left: 0; right: 0; z-index: 1; background-size: cover; background-repeat: no-repeat; background-attachment: fixed; background-position: center; filter: blur(3px); opacity: 0.13}
article{z-index: 3;}
#idx-filter{width: unset;}
#idx-path{vertical-align: text-top;}
#idx-json{display: none;}
.header{background-color: #fff;}
.material-icons{vertical-align: middle;}
.load {position: fixed; left: 0px; top: 0px; width: 100%; height: 100%; background: #EEE; z-index: 9999}
.lds {display: inline-block; width: 80px; height: 80px; position: fixed; left: 47.5%; top: 47.5%;}
.lds:after {content: " "; display: block; width: 64px; height: 64px; margin: 8px; border-radius: 50%; border: 6px solid #ccc; border-color: #ccc transparent; animation: lds 1.2s linear infinite;}
@keyframes lds {0% {transform: rotate(0deg);}100% {transform: rotate(360deg);}}
</style>
<link id="idx-theme" rel="stylesheet" href="" crossorigin="anonymous">
</head>
<body>
<div class="load">
<div class="lds"></div>
</div>
<header>
<ul>
<li>
<span class="material-icons" style="font-size: 36px;">drive_file_move</span>
<span id="idx-path"></span>
</li>
<li></li>
<li></li>
<li></li>
<li>
<span class="material-icons">filter_alt</span>
<input id="idx-filter" type="text" placeholder="Filter">
</li>
<li>
<span class="material-icons">palette</span>
<select id="idx-stheme" style="width:auto;">
<option value="aqua">Aqua</option>
<option value="black">Black</option>
<option value="blue">Blue</option>
<option value="dark2">Dark</option>
<option value="" selected>Default</option>
<option value="fuchsia">Fuchsia</option>
<option value="gray">Gray</option>
<option value="green">Green</option>
<option value="lime">Lime</option>
<option value="maroon">Maroon</option>
<option value="navy">Navy</option>
<option value="olive">Olive</option>
<option value="orange">Orange</option>
<option value="purple">Purple</option>
<option value="red">Red</option>
<option value="silver">Silver</option>
<option value="teal">Teal</option>
<option value="yellow">Yellow</option>
</select>
</li>
</ul>
</header>
<main>
<section></section>
<article>
<table>
<thead>
<tr>
<th class="header"><span class="material-icons">filter_list</span></th>
<th class="header">Name<span class="material-icons">filter_list</span></th>
<th class="header">Date<span class="material-icons">filter_list</span></th>
<th class="header">Size<span class="material-icons">filter_list</span></th>
</tr>
</thead>
<tbody></tbody>
</table>
</article>
</main>
<footer>
<div id="idx-json">

BIN
img/StrapIndex.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
img/milligram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
img/milligram_bg_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

BIN
img/milligram_bg_maroon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

BIN
img/min.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
img/normalize.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
img/picnic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

16
nginx.example.conf Normal file
View File

@ -0,0 +1,16 @@
#Example config for the index
location ~ /downloads(.*)/$ {
autoindex on;
#Send the data in JSON
autoindex_format json;
addition_types application/json;
#Calling from SERVERNAME/autoindex/*
add_before_body /autoindex/header.html;
add_after_body /autoindex/footer.html;
#Need to tell that we are sending HTML
add_header Content-Type text/html;
}