Files
q-tools/web/SimpleWebPage.py
2021-05-26 19:34:15 +03:00

912 lines
26 KiB
Python
Executable File

#!/usr/bin/env python3
# coding=utf-8
""" A script that creates an index for a folder.
"""
import os
import sys
import time
import string
import urllib.parse
from glob import fnmatch
import base64
import random
VERSION = "20210128"
IMAGE_EXTENSIONS = ["png", "gif", "jpg", "jpeg", "tif", "tiff"]
AUDIO_EXTENSIONS = ["wav", "mp3", "ogg"]
VIDEO_EXTENSIONS = ["mp4", "ogg", "webm"]
def setup():
""" Setup the command line options """
from argparse import ArgumentParser
parser = ArgumentParser(
epilog="Recursively generate indexes: \n# find . -type d -not -path '*/.*' -exec SimpleWebPage -f \{\} \;"
)
parser.add_argument(
"-f",
action="store_true",
dest="overwrite",
default=False,
help="Overwrite existing index file, even if it's not generated with SimpleWebPage. By default, if file is generated with SimpleWebPage it will be overwritten!",
)
parser.add_argument(
"-H",
action="store_true",
dest="hidden",
default=False,
help="Show hidden files",
)
parser.add_argument(
"-t",
type=str,
dest="title",
default=None,
help="Name for the title (Default: Folder name)",
)
parser.add_argument(
"-o",
type=str,
dest="filename",
default="index.html",
help="Output filename (Default: index.html)",
)
parser.add_argument(
"-p",
action="store_false",
dest="parent",
default=True,
help="Do no print .. link for parent folder.",
)
parser.add_argument(
"--password",
type=str,
dest="password",
default=None,
help="Set a password to view page. The file list will be written to a randomly generated filename. Note: this is not secure, as the target link will still be unprotected if known.",
)
parser.add_argument(
"-r",
action="store_true",
dest="recursive",
default=False,
help="Include all files recursively in the list. Do not include any folders.",
)
parser.add_argument(
"--images",
action="store_true",
dest="images",
default=False,
help="Show images with <img> tags",
)
parser.add_argument(
"--media",
action="store_true",
dest="media",
default=False,
help="Make media playable",
)
parser.add_argument(
"--include",
"-i",
action="store",
dest="includes",
default=["*"],
help="Glob match for files to be included in the table. ex. *.jpg. You can pass several includes.",
nargs="*",
)
parser.add_argument("--version", action="version", version=VERSION)
parser.add_argument(
"path",
type=str,
action="store",
default=os.path.abspath("."),
nargs="?",
help="Root path of the index",
)
options = parser.parse_args()
options.path = os.path.abspath(options.path)
if options.title == None:
options.title = os.path.basename(options.path)
return options
def setup2HTML(opts):
return '<meta name="SimpleWebPageSetup" content="%s"/>' % ";".join(
[
"hidden=%s" % opts.hidden,
"parent=%s" % opts.parent,
"title=%s" % urllib.parse.quote(opts.title),
"images=%s" % opts.images,
"media=%s" % opts.media,
]
)
def HTML2setup(opts):
""" returns new opts and was it able to read HTML """
try:
read_config = False
with open(os.path.join(opts.path, opts.filename), "rt") as f:
for l in f.readlines():
if l.find('<meta name="SimpleWebPageSetup"') > -1:
content = l[l.find('name="SimpleWebPageSetup"') :]
for s in content.split('"')[3].split(";"):
(k, v) = s.split("=", 1)
if k == "hidden":
opts.hidden = v == "True"
if k == "parent":
opts.parent = v == "True"
if k == "title":
opts.title = urllib.parse.unquote(v)
if k == "images":
opts.images = v == "True"
if k == "media":
opts.media = v == "True"
read_config = True
print("Reading options from existing " + opts.filename)
break
return (opts, read_config)
except:
return (opts, False)
def get_files_and_folders(opts):
if opts.recursive:
rdirs = []
rfiles = []
rpath = None
for path, dirs, files in os.walk(opts.path):
if rpath == None:
rpath = path
if not opts.hidden:
files = [f for f in files if not f.startswith(".")]
dirs[:] = [d for d in dirs if not d.startswith(".")]
files = [os.path.join(os.path.relpath(path, opts.path), f) for f in files]
files = [f[2:] if f.startswith("./") else f for f in files]
rfiles.extend(files)
return rdirs, rfiles, rpath
else:
for path, dirs, files in os.walk(opts.path):
if not opts.hidden:
files = [f for f in files if not f.startswith(".")]
dirs = [d for d in dirs if not d.startswith(".")]
return dirs, files, path
def generate_index(opts):
opts.password_filename = ""
dirs, files, path = get_files_and_folders(opts)
if opts.password != None:
opts.password_filename = opts.filename
opts.filename = generate_password_page(path, opts.filename, opts.password)
existing_config = False
if opts.filename in files:
opts, existing_config = HTML2setup(opts)
if not existing_config and not opts.overwrite:
print(
opts.filename
+ " exists, and not generated with SimpleWebPage. Exiting."
)
sys.exit(1)
files = [f for f in files if f != opts.filename]
files = [f for f in files if f != opts.password_filename]
files = match_files(files, opts.includes)
dirs = match_files(dirs, opts.includes)
dirs.sort()
files.sort()
files.sort(key=lambda x: x.find("/") > 0)
with open(os.path.join(path, opts.filename), "wt") as f:
f.write(get_header(opts))
if opts.parent:
f.write(get_pathlink(path, ".."))
for di in dirs:
f.write(get_pathlink(path, di))
for fi in files:
f.write(get_filelink(path, fi, opts.images, opts.media))
f.write(get_footer())
f.write(get_wget_lines(files))
f.close()
return
def generate_password_page(path, password_file, password):
def ha(p):
h = 0
for c in p:
h = ((h << 5) - h) + ord(c)
h = h & 0xFFFFFFFF
if h > (1 << 31) - 1:
h -= 1 << 32
return h
def scramble(p, t):
p += t
s = ""
for i in range(len(t)):
s += chr(ord(p[i]) + ord(t[i]))
return s
def enc(s):
return base64.b64encode(s.encode("latin1")).decode("ascii")
def random_string(stringLength=16):
letters = string.ascii_lowercase + string.digits
return "".join(random.choice(letters) for i in range(stringLength))
def get_target(filename):
splitted = os.path.splitext(filename)
return (splitted[0], random_string(), splitted[1])
target_base, target_middle, target_ext = get_target(password_file)
secret = "{}:{}".format(ha(password), enc(scramble(password, target_middle)))
with open(os.path.join(path, password_file), "wt") as f:
f.write(get_password_page(secret, target_base, target_ext))
return "{}.{}{}".format(target_base, target_middle, target_ext)
def get_filelink(path, fname, images=False, media=False):
if os.path.islink(os.path.join(path, fname)) and not os.path.exists(
os.path.join(path, fname)
):
(fsize, fsstr, fsstrb, fdstr) = (0, "NA", "NA", "NA")
else:
fsize = os.path.getsize(os.path.join(path, fname))
fsstr = sizeof(fsize)
fsstrb = str(fsize)
fdate = time.localtime(os.path.getmtime(os.path.join(path, fname)))
fdstr = time.strftime("%Y/%m/%d %H:%M:%S", fdate)
fname_str = fname
if images and is_imagefile(fname):
fname_str = get_imagestr(fname)
if media and is_audiofile(fname):
fname_str = get_audiostr(fname)
if media and is_videofile(fname):
fname_str = get_videostr(fname)
return (
'<tr><td><a class="link_file" href="%s">%s</a><td class="right">%s</td><td class="right bytes">%s</td><td class="right">%s</td></tr>\n'
% (urllib.parse.quote(fname), fname_str, fsstr, fsstrb, fdstr)
)
def get_wget_lines(files):
wget = "\n<!--\n"
for f in files:
wget += "#FILE %s\n" % (urllib.parse.quote(f),)
wget += "#WGET URL=[insert URL] && curl $URL | grep '^#FILE ' | cut -c7- | sed \"s,^,$URL,\" | xargs -n1 wget\n"
wget += "-->\n"
return wget
def get_imagestr(fname):
return '<img src="%s" title="%s"/>' % (urllib.parse.quote(fname), fname)
def get_audiostr(fname):
return '%s<br><audio src="%s" controls class=audio></audio>' % (
fname,
urllib.parse.quote(fname),
)
def get_videostr(fname):
return '%s<br><video class=video controls><source src="%s"></video>' % (
fname,
urllib.parse.quote(fname),
)
def get_pathlink(path, dname):
fdate = time.localtime(os.path.getmtime(os.path.join(path, dname)))
fdstr = time.strftime("%Y/%m/%d %H:%M:%S", fdate)
return (
'<tr><td><a class="link_dir" href="%s">&#8627;&nbsp;%s/</a><td class="right">[DIR]</td><td class="right bytes">0</td><td class="right">%s</td></tr>\n'
% (urllib.parse.quote(dname), dname, fdstr)
)
def get_password_page(secret, target_base, target_ext):
return (
"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, viewport-fit=cover">
<title>Password required</title>
<script language="JavaScript">
Object.defineProperty(String.prototype, 'hashCode', {
value: function() {
var hash = 0, i, chr;
for (i = 0; i < this.length; i++) {
chr = this.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
}
});
function isMobile () {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
function scramble(p,t) {
p += t;
var sstr = '';
for (var i=0; i < t.length; i++) {
sstr += String.fromCharCode(p.charCodeAt(i) + t.charCodeAt(i));
}
return sstr;
}
function unscramble(p,t) {
var sstr = '';
var schr = '';
for (var i=0; i < t.length; i++) {
schr = String.fromCharCode(t.charCodeAt(i) - p.charCodeAt(i));
sstr += schr;
p += schr;
}
return sstr;
}
function pw_event(element) {
if(event.key === 'Enter') {
pw(element);
}
}
function pw(element) {
var pw = "SECRET";
if (element.value) {
pw = pw.split(':',2);
if (element.value.hashCode() == parseInt(pw[0])) {
let unscrambled = "TARGET_BASE." + unscramble(element.value, atob(pw[1])) + "TARGET_EXT"
if (isMobile()) {
document.location.replace(unscrambled);
return
}
document.getElementById("body").innerHTML = '';
var frame = document.createElement("iframe");
frame.src = unscrambled;
document.getElementById("body").appendChild(frame);
} else {
document.getElementById("message").innerHTML = "No match";
setTimeout(clear_message, 5000);
}
}
}
function clear_message() {
document.getElementById("message").innerHTML = '';
}
function newPass(password, target) {
console.log(
' var pw = "' + password.hashCode().toString() + ':' +
btoa(scramble(password, target)) + '";'
);
}
//console.log("Use newPass('pass','page.html'); to get a new hash");
</script>
<style>
iframe {
width: 100vw;
height: 100vh;
}
#body {
margin: 0;
overflow-x: hidden;
overflow-y: hidden;
}
#content {
text-align: center;
padding-top: 4rem;
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
font-weight: bold;
}
#message {
margin-top: 1rem;
}
#button {
margin-top: 1rem;
}
</style>
</head>
<body id="body">
<div id="content">
<input type="password" placeholder="Password" class="password" onkeydown="pw_event(this)" id="password" onFocus="this.select()" autofocus/>
<br><button id="button" type="button" onclick="pw(document.getElementById('password'));">Enter</button>
<div id="message"></div>
<noscript><br>Javascript is required to access this area. Yours seems to be disabled.</noscript>
</div>
</body>
</html>""".replace(
"TARGET_EXT", target_ext, 1
)
.replace("TARGET_BASE", target_base, 1)
.replace("SECRET", secret, 1)
)
def get_header(opts):
header = (
"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, viewport-fit=cover">
<meta name="generator" content="SimpleWebPage """
+ VERSION
+ """" />
"""
+ setup2HTML(opts)
+ """
<title>"""
+ opts.title
+ """</title>
<style>
/* Style modified from: https://css-tricks.com/snippets/php/display-styled-directory-contents/ */
* {
padding: 0;
margin: 0;
}
body {
color: #222;
font: 14px monospace;
padding: 20px;
background: #CCC;
}
h1 {
text-align: center;
padding: 20px 0 12px 0;
margin: 0;
font-family: sans-serif;
}
.container {
box-shadow: 0 5px 10px -5px rgba(0,0,0,0.5);
position: relative;
background: #eee;
border-top-left-radius: 6em;
}
table {
background-color: #F3F3F3;
border-collapse: collapse;
width: 100%;
margin: 15px 0;
}
th {
background-color: #EB6000;
color: #FFF;
cursor: pointer;
padding: 5px 10px;
}
td {
padding-right: 5px;
}
a {
text-decoration: none;
}
td a {
color: #EB6000;
display: block;
padding: 5px 10px;
}
th a {
padding-left: 0
}
td:first-of-type a {
padding-left: 5px;
}
th:first-of-type {
padding-left: 5px;
}
tr:nth-of-type(odd) {
background-color: #DDD;
}
tr:hover td {
background-color:#BBB;
}
tr:hover td a {
color: #222;
}
.right {
text-align: right;
}
.bytes {
font-size: xx-small;
}
.audio {
width: 90%;
}
.video {
width: 320;
height: 240;
}
</style>
<script type="text/javascript">
/*
Table sorting script by Joost de Valk, check it out at http://www.joostdevalk.nl/code/sortable-table/.
Based on a script from http://www.kryogenix.org/code/browser/sorttable/.
Distributed under the MIT license: http://www.kryogenix.org/code/browser/licence.html .
Edited, removed image support and a tags from headers, Ville Rantanen 2015
Copyright (c) 1997-2007 Stuart Langridge, Joost de Valk.
Version 1.5.7
*/
/* You can change these values */
var europeandate = true;
var alternate_row_colors = false;
/* Don't change anything below this unless you know what you're doing */
addEvent(window, "load", sortables_init);
var SORT_COLUMN_INDEX;
var thead = false;
function sortables_init() {
// Find all tables with class sortable and make them sortable
if (!document.getElementsByTagName) return;
tbls = document.getElementsByTagName("table");
for (ti=0;ti<tbls.length;ti++) {
thisTbl = tbls[ti];
if (((' '+thisTbl.className+' ').indexOf("sortable") != -1) && (thisTbl.id)) {
ts_makeSortable(thisTbl);
}
}
}
function ts_makeSortable(t) {
if (t.rows && t.rows.length > 0) {
if (t.tHead && t.tHead.rows.length > 0) {
var firstRow = t.tHead.rows[t.tHead.rows.length-1];
thead = true;
} else {
var firstRow = t.rows[0];
}
}
if (!firstRow) return;
// We have a first row: assume it's the header, and make its contents clickable links
for (var i=0;i<firstRow.cells.length;i++) {
var cell = firstRow.cells[i];
var txt = ts_getInnerText(cell);
if (cell.className != "unsortable" && cell.className.indexOf("unsortable") == -1) {
cell.innerHTML = '<span class="sortheader" onclick="ts_resortTable(this, '+i+');return false;">'+txt+'<span class="sortarrow">&nbsp;&nbsp;&varr;</span></span>';
}
}
if (alternate_row_colors) {
alternate(t);
}
}
function ts_getInnerText(el) {
if (typeof el == "string") return el;
if (typeof el == "undefined") { return el };
if (el.innerText) return el.innerText; //Not needed but it is faster
var str = "";
var cs = el.childNodes;
var l = cs.length;
for (var i = 0; i < l; i++) {
switch (cs[i].nodeType) {
case 1: //ELEMENT_NODE
str += ts_getInnerText(cs[i]);
break;
case 3: //TEXT_NODE
str += cs[i].nodeValue;
break;
}
}
return str;
}
function ts_resortTable(lnk, clid) {
var span;
for (var ci=0;ci<lnk.childNodes.length;ci++) {
if (lnk.childNodes[ci].tagName && lnk.childNodes[ci].tagName.toLowerCase() == 'span') span = lnk.childNodes[ci];
}
var spantext = ts_getInnerText(span);
var td = lnk.parentNode;
var column = clid || td.cellIndex;
var t = getParent(td,'TABLE');
// Work out a type for the column
if (t.rows.length <= 1) return;
var itm = "";
var i = 0;
while (itm == "" && i < t.tBodies[0].rows.length) {
var itm = ts_getInnerText(t.tBodies[0].rows[i].cells[column]);
itm = trim(itm);
if (itm.substr(0,4) == "<!--" || itm.length == 0) {
itm = "";
}
i++;
}
if (itm == "") return;
sortfn = ts_sort_caseinsensitive;
if (itm.match(/^\d\d[\/\.-][a-zA-z][a-zA-Z][a-zA-Z][\/\.-]\d\d\d\d$/)) sortfn = ts_sort_date;
if (itm.match(/^\d\d[\/\.-]\d\d[\/\.-]\d\d\d{2}?$/)) sortfn = ts_sort_date;
if (itm.match(/^-?[£$€Û¢´]\d/)) sortfn = ts_sort_numeric;
if (itm.match(/^-?(\d+[,\.]?)+(E[-+][\d]+)?%?$/)) sortfn = ts_sort_numeric;
SORT_COLUMN_INDEX = column;
var firstRow = new Array();
var newRows = new Array();
for (k=0;k<t.tBodies.length;k++) {
for (i=0;i<t.tBodies[k].rows[0].length;i++) {
firstRow[i] = t.tBodies[k].rows[0][i];
}
}
for (k=0;k<t.tBodies.length;k++) {
if (!thead) {
// Skip the first row
for (j=1;j<t.tBodies[k].rows.length;j++) {
newRows[j-1] = t.tBodies[k].rows[j];
}
} else {
// Do NOT skip the first row
for (j=0;j<t.tBodies[k].rows.length;j++) {
newRows[j] = t.tBodies[k].rows[j];
}
}
}
newRows.sort(sortfn);
if (span.getAttribute("sortdir") == 'down') {
ARROW = '&nbsp;&nbsp;&uarr;';
newRows.reverse();
span.setAttribute('sortdir','up');
} else {
ARROW = '&nbsp;&nbsp;&darr;';
span.setAttribute('sortdir','down');
}
// We appendChild rows that already exist to the tbody, so it moves them rather than creating new ones
// don't do sortbottom rows
for (i=0; i<newRows.length; i++) {
if (!newRows[i].className || (newRows[i].className && (newRows[i].className.indexOf('sortbottom') == -1))) {
t.tBodies[0].appendChild(newRows[i]);
}
}
// do sortbottom rows only
for (i=0; i<newRows.length; i++) {
if (newRows[i].className && (newRows[i].className.indexOf('sortbottom') != -1))
t.tBodies[0].appendChild(newRows[i]);
}
// Delete any other arrows there may be showing
var allspans = document.getElementsByTagName("span");
for (var ci=0;ci<allspans.length;ci++) {
if (allspans[ci].className == 'sortarrow') {
if (getParent(allspans[ci],"table") == getParent(lnk,"table")) { // in the same table as us?
allspans[ci].innerHTML = '&nbsp;&nbsp;&varr;';
}
}
}
span.innerHTML = ARROW;
alternate(t);
}
function getParent(el, pTagName) {
if (el == null) {
return null;
} else if (el.nodeType == 1 && el.tagName.toLowerCase() == pTagName.toLowerCase()) {
return el;
} else {
return getParent(el.parentNode, pTagName);
}
}
function sort_date(date) {
// y2k notes: two digit years less than 50 are treated as 20XX, greater than 50 are treated as 19XX
dt = "00000000";
if (date.length == 11) {
mtstr = date.substr(3,3);
mtstr = mtstr.toLowerCase();
switch(mtstr) {
case "jan": var mt = "01"; break;
case "feb": var mt = "02"; break;
case "mar": var mt = "03"; break;
case "apr": var mt = "04"; break;
case "may": var mt = "05"; break;
case "jun": var mt = "06"; break;
case "jul": var mt = "07"; break;
case "aug": var mt = "08"; break;
case "sep": var mt = "09"; break;
case "oct": var mt = "10"; break;
case "nov": var mt = "11"; break;
case "dec": var mt = "12"; break;
// default: var mt = "00";
}
dt = date.substr(7,4)+mt+date.substr(0,2);
return dt;
} else if (date.length == 10) {
if (europeandate == false) {
dt = date.substr(6,4)+date.substr(0,2)+date.substr(3,2);
return dt;
} else {
dt = date.substr(6,4)+date.substr(3,2)+date.substr(0,2);
return dt;
}
} else if (date.length == 8) {
yr = date.substr(6,2);
if (parseInt(yr) < 50) {
yr = '20'+yr;
} else {
yr = '19'+yr;
}
if (europeandate == true) {
dt = yr+date.substr(3,2)+date.substr(0,2);
return dt;
} else {
dt = yr+date.substr(0,2)+date.substr(3,2);
return dt;
}
}
return dt;
}
function ts_sort_date(a,b) {
dt1 = sort_date(ts_getInnerText(a.cells[SORT_COLUMN_INDEX]));
dt2 = sort_date(ts_getInnerText(b.cells[SORT_COLUMN_INDEX]));
if (dt1==dt2) {
return 0;
}
if (dt1<dt2) {
return -1;
}
return 1;
}
function ts_sort_numeric(a,b) {
var aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
aa = clean_num(aa);
var bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
bb = clean_num(bb);
return compare_numeric(aa,bb);
}
function compare_numeric(a,b) {
var a = parseFloat(a);
a = (isNaN(a) ? 0 : a);
var b = parseFloat(b);
b = (isNaN(b) ? 0 : b);
return a - b;
}
function ts_sort_caseinsensitive(a,b) {
aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase();
bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase();
if (aa==bb) {
return 0;
}
if (aa<bb) {
return -1;
}
return 1;
}
function ts_sort_default(a,b) {
aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
if (aa==bb) {
return 0;
}
if (aa<bb) {
return -1;
}
return 1;
}
function addEvent(elm, evType, fn, useCapture)
// addEvent and removeEvent
// cross-browser event handling for IE5+, NS6 and Mozilla
// By Scott Andrew
{
if (elm.addEventListener){
elm.addEventListener(evType, fn, useCapture);
return true;
} else if (elm.attachEvent){
var r = elm.attachEvent("on"+evType, fn);
return r;
} else {
alert("Handler could not be removed");
}
}
function clean_num(str) {
str = str.replace(new RegExp(/[^-?0-9.]/g),"");
return str;
}
function trim(s) {
return s.replace(/^\s+|\s+$/g, "");
}
function alternate(table) {
// Take object table and get all it's tbodies.
var tableBodies = table.getElementsByTagName("tbody");
// Loop through these tbodies
for (var i = 0; i < tableBodies.length; i++) {
// Take the tbody, and get all it's rows
var tableRows = tableBodies[i].getElementsByTagName("tr");
// Loop through these rows
// Start at 1 because we want to leave the heading row untouched
for (var j = 0; j < tableRows.length; j++) {
// Check if j is even, and apply classes for both possible results
if ( (j % 2) == 0 ) {
if ( !(tableRows[j].className.indexOf('odd') == -1) ) {
tableRows[j].className = tableRows[j].className.replace('odd', 'even');
} else {
if ( tableRows[j].className.indexOf('even') == -1 ) {
tableRows[j].className += " even";
}
}
} else {
if ( !(tableRows[j].className.indexOf('even') == -1) ) {
tableRows[j].className = tableRows[j].className.replace('even', 'odd');
} else {
if ( tableRows[j].className.indexOf('odd') == -1 ) {
tableRows[j].className += " odd";
}
}
}
}
}
}
</script>
</head>
<body>
<div class="container">
<h1>"""
+ opts.title
+ """</h1>
<table class="sortable" id="fileList"><thead><tr><th>Name</th><th class="right">Size</th><th class="right">Size B</th><th class="right">Modified</th></tr></thead><tbody>
"""
)
return header
def get_footer():
return """</tbody></table></div>
</body></html>"""
def is_imagefile(fname):
for ext in IMAGE_EXTENSIONS:
if fname.lower().endswith(ext):
return True
return False
def is_audiofile(fname):
for ext in AUDIO_EXTENSIONS:
if fname.lower().endswith(ext):
return True
return False
def is_videofile(fname):
for ext in VIDEO_EXTENSIONS:
if fname.lower().endswith(ext):
return True
return False
def match_files(files, glob_list):
matched = []
for f in files:
for g in glob_list:
if fnmatch.fnmatch(f, g):
matched.append(f)
break
return matched
def sizeof(num):
for x in ["&nbsp;B", "KB", "MB", "GB", "TB"]:
if num < 1024.0:
if x == "&nbsp;B":
return "%d&nbsp;%s" % (num, x)
return "%3.1f&nbsp;%s" % (num, x)
num /= 1024.0
if __name__ == "__main__":
opts = setup()
generate_index(opts)