#!/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 json import re import urllib.parse from glob import fnmatch import base64 import random from pprint import pprint VERSION = "20211002" IMAGE_EXTENSIONS = ["png", "gif", "jpg", "jpeg", "tif", "tiff"] AUDIO_EXTENSIONS = ["wav", "mp3", "ogg"] VIDEO_EXTENSIONS = ["mp4", "ogg", "webm"] SAFE_OPTS = ( "hidden", "title", "parent", "recursive", "images", "media", "includes", "no_readme", ) 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( "--no-readme", action="store_true", dest="no_readme", default=False, help="Do not show README.md on the page", ) 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 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 print_setup(opts): print("Current configuration:") pprint(setup2safe(opts)) def setup2safe(opts): safe_opts = {} opts_dict = vars(opts) for key in opts_dict: if key in SAFE_OPTS: safe_opts[key] = opts_dict[key] return safe_opts def setup2JSON(opts): return json.dumps(setup2safe(opts)) 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(' -1: content = l[l.find("content=") :].rstrip("'/>\n") config = json.loads(content[9:]) for key in config: if key in SAFE_OPTS: setattr(opts, key, config[key]) read_config = True print("Read options from existing " + opts.filename) break return (opts, read_config) except Exception as e: print("Error parsing configuration") print(e) 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) 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) # Re-read files, with probably changed opts dirs, files, path = get_files_and_folders(opts) print_setup(opts) 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) readme = get_readme(path, opts.no_readme) 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(readme)) f.write(get_download_lines(files, recursive=opts.recursive)) 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 ( '  %s%s%s%s\n' % (urllib.parse.quote(fname), fname_str, fsstr, fsstrb, fdstr) ) def get_download_lines(files, recursive=False): s = "\n\n" return s def get_imagestr(fname): return '' % (urllib.parse.quote(fname), fname) def get_audiostr(fname): return '%s
' % ( fname, urllib.parse.quote(fname), ) def get_videostr(fname): return '%s
' % ( 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 ( '↳ %s/[DIR]0%s\n' % (urllib.parse.quote(dname), dname, fdstr) ) def get_password_page(secret, target_base, target_ext): return ( """ Password required

""".replace( "TARGET_EXT", target_ext, 1 ) .replace("TARGET_BASE", target_base, 1) .replace("SECRET", secret, 1) ) def get_readme(path, no_read): if no_read: return "" if not os.path.exists("README.md"): return "" with open("README.md", "rt") as fp: return "

README.md

{}
".format( re.sub( r"(https?:\/\/[\w\.,\-\@?^=%&:/~\+#]+)", '\\1', fp.read().strip(), flags=re.IGNORECASE, ) ) def get_header(opts): opts_str = setup2JSON(opts) js_code = """ """ css_style = """ """ header = ( """ {title} {css_style} {js_code}

{title}

""" ).format( title=opts.title, config=opts_str, program="SimpleWebPage", version=VERSION, js_code=js_code, css_style=css_style, ) return header def get_footer(readme): return """
NameSizeSize BModified
{README}
""".format( README=readme ) 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 [" B", "KB", "MB", "GB", "TB"]: if num < 1024.0: if x == " B": return "%d %s" % (num, x) return "%3.1f %s" % (num, x) num /= 1024.0 if __name__ == "__main__": opts = setup() generate_index(opts)