#!/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}'
"""
|
Name |
Size |
Modified
|
|
|
.. |
— |
— |
|
""")
# 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"""
|
{entry.name}
|
{size_pretty} |
|
|
""")
index_file.write("""
""")
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)