Skip to content

Instantly share code, notes, and snippets.

@hdf
Last active July 22, 2020 16:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hdf/780be19d30e75b709c24a7a8bfc653b3 to your computer and use it in GitHub Desktop.
Save hdf/780be19d30e75b709c24a7a8bfc653b3 to your computer and use it in GitHub Desktop.
Generate html file with foldable recursive directory listing. (For directories with no auto indexing.)
import os, sys
from datetime import datetime
from zipfile import ZipFile
dir = '.'
out = 'dir.html'
if len(sys.argv) > 1:
dir = sys.argv[1]
if len(sys.argv) > 2:
out = sys.argv[2]
out = dir.rstrip('/') + '/' + out.lstrip('/')
header = '''<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="ASCII">
<title>Directory Index of ''' + dir + '''</title>
<style>
* {
font-family: 'Verdana', sans-serif;
margin: 0;
padding: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html {
color: #61666c;
font-weight: 300;
font-size: 1em;
line-height: 1.5em;
}
body {
margin: 0 auto;
padding: 20px 0 20px 0;
max-width: 80vw;
}
h1 {
font-weight: 200;
text-align: center;
font-size: 1.4em;
margin-bottom: 1.4em;
padding: 8px;
border-radius: 12px;
box-shadow: inset 0 5px 5px rgba(0,0,0,0.05),0 0 5px rgba(0,0,0,0.8);
}
a {
color: #5f5f5f;
text-decoration: none;
}
a:hover {
color: #000;
}
a.clear, a.clear:link, a.clear:visited {
color: #666;
padding: 2px 0;
font-weight: 400;
font-size: 14px;
margin: 0 0 0 20px;
line-height: 14px;
display: inline-block;
border-bottom: transparent 1px solid;
vertical-align: -10px;
-webkit-transition: all 300ms ease-in;
-moz-transition: all 300ms ease-in;
-ms-transition: all 300ms ease-in;
-o-transition: all 300ms ease-in;
transition: all 300ms ease-in;
}
.collapsible {
background-color: #777;
color: white;
cursor: pointer;
border: none;
text-align: left;
outline: none;
font-size: 16px;
width: 100%;
}
.active, .collapsible:hover {
background-color: #555;
}
.collapsible::before {
padding: 0 4px 0 4px;
}
.collapsible:not(.active)::before {
content: '+';
}
.active::before {
content: '-';
}
.total {
margin: 10px 4px 5px 0px;
background-color: #ddd;
padding: 0px 2px 1px 4px;
}
.mod {
padding-right: 4px;
margin-left: 8px;
}
.content {
display: none;
overflow: hidden;
background-color: #f1f1f1;
padding-left: 4px;
}
.content[style*="display: block;"] + p {
border-top: 3px solid white;
}
@keyframes popin {
0% {background-color: #ddd;}
50% {background-color: #fff;}
100% {background-color: #f1f1f1;}
}
.zip {
opacity: 0.7;
background-color: #f9f9f9;
padding-left: 1em;
}
.zipfile {
background-color: #f1f1f1;
color: #61666c;
}
.zipfile:hover {
background-color: #ddd;
}
.content>p {
display: flex;
border-bottom: 1px solid white;
}
.content>p>span {
flex: 0 0 auto;
}
.content>p>span:nth-of-type(1) {
flex: 1 1 auto;
}
button.collapsible>.mod {
float: right;
}
</style>
</head>
<body>
<h1>Directory: ''' + dir + '''</h1>
<div class="content" style="display: block;">
'''
footer = '''</div>
<script>
(function() {
let h = window.location.hash.substring(1);
var loading = true;
let path = window.location.pathname;
path = path.substr(0, path.lastIndexOf('/')+1);
document.title = 'Directory Index of ' + path;
document.getElementsByTagName('h1')[0].innerHTML = 'Directory: ' + path;
let e = document.getElementsByClassName('collapsible');
for (let i = 0; i < e.length; i++) {
if (e[i].classList.contains('zipfile')) {
e[i].getElementsByTagName('a')[0].addEventListener('click', function(e) {
e.stopPropagation();
});
}
e[i].addEventListener('click', function() {
this.classList.toggle('active');
this.nextElementSibling.style.display = (this.nextElementSibling.style.display === 'block')?'none':'block';
if (this.nextElementSibling.style.display === 'block' && (!loading || h === this.id)) {
this.nextElementSibling.style.animation = 'popin 1s ease-in-out';
window.location.hash = '#' + this.id;
}
});
if (h.includes(e[i].id + '/') || h === e[i].id) {
setTimeout(function(e) { e.click(); }, 10, e[i]);
}
}
e = document.getElementsByClassName('mod');
for (let i = 0; i < e.length; i++) {
e[i].setAttribute('title', 'Last modified');
}
setTimeout(function() { loading = false; }, 30);
})();
</script>
</body>
</html>
'''
def format_size(size, decimal_places=2):
for unit in ['Bytes','KB','MB','GB','TB']:
if size < 1024.0:
break
size /= 1024.0
return f'{size:.{decimal_places}f}'.rstrip('0').rstrip('.') + ' ' + unit
def generate_tree(path, indent=0):
html = ''
ind = '' + (' ' * indent)
total = 0
files = []
dn = 0
fn = 0
tdn = 0
tfn = 0
for file in os.listdir(path):
rel = os.path.normpath(path + '/' + file).replace('\\', '/')
mod = '<span class="mod">' + datetime.fromtimestamp(os.path.getmtime(rel)).strftime('%Y.%m.%d %H:%M:%S.%f')[:-7] + '</span>'
if rel[0:6] == 'hidden' or rel[0:4] == 'dir.':
continue
if os.path.isdir(rel):
r, t, rdn, rfn, rtdn, rtfn = generate_tree(rel, indent+1)
html += ind + ' <button class="collapsible" id="%s">%s <span title="Contains a total of %s folder(s) and %s file(s), %s Bytes">(%s folder(s) and %s file(s), %s)</span> %s</button>\n' %\
(rel, file, rtdn, rtfn, f'{t:,d}'.replace(',', ' '), rdn, rfn, format_size(t), mod)
html += ind + ' <div class="content" style="margin-left: %sem;">\n' % (indent+1)
html += r + ind + ' </div>\n'
total += t
dn += 1
tdn += rtdn + 1
tfn += rtfn
else:
size = os.path.getsize(rel)
total += size
fn += 1
tfn += 1
z = ''
zn = ''
collapsible = ''
if rel[-4:] == '.zip':
collapsible = ' class="collapsible zipfile" id="%s"' % (rel)
z = '<div class="zip content">\n'
zc = 0
zf = 0
with ZipFile(rel, 'r') as zipObj:
listOfiles = zipObj.infolist()
for elem in listOfiles:
if elem.is_dir():
zf += 1
continue
zc += 1
z += '<p><a href="./%s">%s</a><span></span> <span title="%s Bytes / %s Compressed">(%s)</span> <span class="mod">%s</span></p>\n' % (rel, elem.filename, f'{elem.file_size:,d}'.replace(',', ' '), f'{elem.compress_size:,d}'.replace(',', ' '), format_size(elem.file_size), datetime(*elem.date_time).strftime('%Y.%m.%d %H:%M:%S.%f')[:-7])
z += '</div>\n'
zn = ' title="(Contains %s files in %s folders)"' % (zc, zf)
files.append(ind + ' <p%s%s><a href="./%s">%s</a><span></span> <span title="%s Bytes">(%s)</span> %s</p>\n%s' % (collapsible, zn, rel, file, f'{size:,d}'.replace(',', ' '), format_size(size), mod, z))
html += ''.join(files)
if indent == 0:
html += ind + ' <p class="total">Total size: <span title="%s Bytes"> %s in %s folder(s) and %s file(s)</span></p>\n' % (f'{total:,d}'.replace(',', ' '), format_size(total), tdn, tfn)
return (html, total, dn, fn, tdn, tfn)
with open(out, 'w') as f:
f.write(header + generate_tree(dir)[0] + footer)
print(out + ' written.')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment