'''
def process_dir(path, opts):
"""Write the index file of `path`; return the subdirectories to descend into."""
output = opts.output_file.lower()
entries, subdirs, icons = [], [], {'go-up'}
try:
with os.scandir(path) as it:
for e in it:
name = e.name
if (name.lower() == output or (not opts.include_hidden and name[0] == '.')
or (opts.exclude_regex and opts.exclude_regex.search(name))):
continue
try:
link, is_dir = e.is_symlink(), e.is_dir()
if opts.filter and not is_dir and not fnmatch(name, opts.filter):
continue
try:
st = e.stat()
except OSError: # dangling symlink, symlink loop or vanished entry
st = None
if (st and not opts.include_hidden
and getattr(st, 'st_file_attributes', 0) & stat.FILE_ATTRIBUTE_HIDDEN):
continue
# world-readable bit (always set on Windows) spares the access() syscall
if not link and not (st and st.st_mode & stat.S_IROTH or os.access(e.path, os.R_OK)):
log(f'*** WARNING *** entry {e.path} is not readable! SKIPPING!')
continue
if is_dir:
icon = 'folder-symlink' if link else 'folder'
if opts.recursive:
subdirs.append(e.path)
elif link:
icon = 'symlink'
elif opts.generic_icons or not e.is_file():
icon = 'generic'
else:
icon = NAME_ICON.get(name) or EXT_ICON.get(os.path.splitext(name)[1].lower(), 'generic')
size, size_pretty, iso, human = -1, '—', '', '-'
if st:
if stat.S_ISREG(st.st_mode):
size = st.st_size
size_pretty = pretty_size(size)
iso, human = format_mtime(st.st_mtime_ns // 1_000_000_000)
except OSError as err:
log('ERROR accessing file:', err, e.path)
continue
if opts.verbose:
log(e.path)
icons.add(icon)
href = quote(name + '/' if is_dir else name, errors='surrogateescape')
entries.append((not is_dir, name, ROW(href, icon, escape(name), size, size_pretty, iso, human)))
except OSError as err:
log(f'cannot read dir {path}: {err}')
return []
entries.sort()
index = os.path.join(path, opts.output_file)
try:
# surrogateescape round-trips undecodable POSIX file names byte-for-byte
with open(index, 'w', encoding='utf-8', errors='surrogateescape') as f:
f.write(render(escape(os.path.basename(path) or path), icons, [row for *_, row in entries]))
except OSError as err:
log(f'cannot create file {index}: {err}')
return sorted(subdirs)
def type_regex(s):
try:
return re.compile(s) if s else None
except re.error as e:
raise argparse.ArgumentTypeError(f'Invalid regular expression: {e}')
def type_dir(s):
if not os.path.isdir(s):
raise argparse.ArgumentTypeError(f'not a directory: {s!r}')
return os.path.abspath(s)
def main(argv=None):
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='?', type=type_dir, default=os.getcwd(),
help='top folder from which to start generating indexes, use current folder if not specified')
parser.add_argument('--filter', '-f',
help='only include files matching glob (directories are always included)')
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)')
parser.add_argument('--include-hidden', '-i', action='store_true',
help='include dot hidden files (FALSE by default)')
parser.add_argument('--exclude-regex', '-x', type=type_regex,
help='exclude files matching regular expression')
parser.add_argument('--generic-icons', '-g', action='store_true',
help='use a single generic icon for all regular files instead of per-type icons '
'(folders and symlinks keep their own icons); produces smaller files')
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')
opts = parser.parse_args(argv)
if os.path.basename(opts.output_file) != opts.output_file or opts.output_file in ('', '.', '..'):
parser.error(f'--output-file must be a plain file name: {opts.output_file!r}')
# iterative DFS: no recursion limit on deep trees; `seen` breaks symlink cycles
pending, seen = [opts.top_dir], set()
while pending:
path = pending.pop()
try:
st = os.stat(path)
except OSError as err:
log(f'cannot access dir {path}: {err}')
continue
key = (st.st_dev, st.st_ino) if st.st_ino else os.path.realpath(path)
if key in seen:
continue
seen.add(key)
if opts.verbose:
log(f'Traversing dir {path}')
pending.extend(reversed(process_dir(path, opts)))
if __name__ == '__main__':
main()