Last active
July 22, 2020 16:53
-
-
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.)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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