#!/usr/bin/env python3 # --- # -handle symlinked files and folders: displayed with custom icons # By default only the current folder is processed. # Use -r or --recursive to process nested folders. import argparse import datetime import os import sys from pathlib import Path from urllib.parse import quote DEFAULT_OUTPUT_FILE = 'nav-index.html' def process_dir(top_dir, opts): glob_patt = opts.filter or '*' path_top_dir: Path path_top_dir = Path(top_dir) index_file = None index_path = Path(path_top_dir, opts.output_file) if opts.verbose: print(f'Traversing dir {path_top_dir.absolute()}') try: index_file = open(index_path, 'w') except Exception as e: print('cannot create file %s %s' % (index_path, e)) return index_file.write("""

""" f'{path_top_dir.name}' """

""") # sort dirs first sorted_entries = sorted(path_top_dir.glob(glob_patt), key=lambda p: (p.is_file(), p.name)) entry: Path for entry in sorted_entries: # don't include index.html in the file listing if entry.name.lower() == opts.output_file.lower(): continue if entry.is_dir() and opts.recursive: process_dir(entry, opts) # From Python 3.6, os.access() accepts path-like objects if (not entry.is_symlink()) and not os.access(str(entry), os.W_OK): print(f"*** WARNING *** entry {entry.absolute()} is not writable! SKIPPING!") continue if opts.verbose: print(f'{entry.absolute()}') size_bytes = -1 ## is a folder size_pretty = '—' last_modified = '-' last_modified_human_readable = '-' last_modified_iso = '' try: if entry.is_file(): size_bytes = entry.stat().st_size size_pretty = pretty_size(size_bytes) if entry.is_dir() or entry.is_file(): last_modified = datetime.datetime.fromtimestamp(entry.stat().st_mtime).replace(microsecond=0) last_modified_iso = last_modified.isoformat() last_modified_human_readable = last_modified.strftime("%c") except Exception as e: print('ERROR accessing file name:', e, entry) continue entry_path = str(entry.name) if entry.is_dir() and not entry.is_symlink(): entry_type = 'folder' if os.name not in ('nt',): # append trailing slash to dirs, unless it's windows entry_path = os.path.join(entry.name, '') elif entry.is_dir() and entry.is_symlink(): entry_type = 'folder-shortcut' print('dir-symlink', entry.absolute()) elif entry.is_file() and entry.is_symlink(): entry_type = 'file-shortcut' print('file-symlink', entry.absolute()) else: entry_type = 'file' index_file.write(f""" """) index_file.write("""
Name Size Modified
..
{entry.name} {size_pretty}
""") if index_file: index_file.close() # bytes pretty-printing UNITS_MAPPING = [ (1024 ** 5, ' PB'), (1024 ** 4, ' TB'), (1024 ** 3, ' GB'), (1024 ** 2, ' MB'), (1024 ** 1, ' KB'), (1024 ** 0, (' byte', ' bytes')), ] def pretty_size(bytes, units=UNITS_MAPPING): """Human-readable file sizes. ripped from https://pypi.python.org/pypi/hurry.filesize/ """ for factor, suffix in units: if bytes >= factor: break amount = int(bytes / factor) if isinstance(suffix, tuple): singular, multiple = suffix if amount == 1: suffix = singular else: suffix = multiple return str(amount) + suffix if __name__ == "__main__": parser = argparse.ArgumentParser(description='''DESCRIPTION: Generate directory index files (recursive is OFF by default). Start from current dir or from folder passed as first positional argument. Optionally filter by file types with --filter "*.py". ''') parser.add_argument('top_dir', nargs='?', action='store', help='top folder from which to start generating indexes, ' 'use current folder if not specified', default=os.getcwd()) parser.add_argument('--filter', '-f', help='only include files matching glob', required=False) parser.add_argument('--output-file', '-o', metavar='filename', default=DEFAULT_OUTPUT_FILE, help=f'Custom output file, by default "{DEFAULT_OUTPUT_FILE}"') parser.add_argument('--recursive', '-r', action='store_true', help="recursively process nested dirs (FALSE by default)", required=False) parser.add_argument('--verbose', '-v', action='store_true', help='***WARNING: can take longer time with complex file tree structures on slow terminals***' ' verbosely list every processed file', required=False) config = parser.parse_args(sys.argv[1:]) process_dir(config.top_dir, config)