Verified Commit ef196458 authored by Sebastian Endres's avatar Sebastian Endres
Browse files

Add create_dirlisting script

parent 33ffcebb
#!/usr/bin/env python3
"""Create a human readable static directory listing for a given folder."""
import argparse
import shutil
import os
from datetime import datetime
from pathlib import Path
from typing import NamedTuple
import jinja2
from utils import YaspinWrapper, existing_dir_path
DIRLISTING_STYLE_NAME = ".dirlisting_style.css"
STYLESHEET_PATH = Path(__file__).parent / "templates" / "dirlisting_style.css"
class FileInfo(NamedTuple):
"""A NamedTuple to describe files."""
path: Path
size: int
date: datetime
@classmethod
def from_dir(cls, path: Path) -> "FileInfo":
"""Return a NamedTuple of file information."""
stat = path.stat()
size = stat.st_size
date = datetime.fromtimestamp(stat.st_mtime)
return FileInfo(path, size, date)
class DirInfo(NamedTuple):
"""A NamedTuple to describe directories."""
path: Path
date: datetime
@classmethod
def from_dir(cls, path: Path) -> "DirInfo":
"""Return a NamedTuple of file information."""
date = datetime.fromtimestamp(path.stat().st_mtime)
return cls(path, date)
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"roots",
nargs="+",
type=existing_dir_path,
help="Directories to create dir listing.",
)
parser.add_argument(
"--debug",
action="store_true",
help="Debug mode",
)
return parser.parse_args()
class Cli:
"""Create a human readable static directory listing for a given folder."""
#: Jinja2 environment for rendeing index.html files.
template_env = jinja2.Environment(
loader=jinja2.FileSystemLoader(Path(__file__).parent / "templates")
)
#: File names, that will be omitted.
IGNORE_FILES = {
"index.html",
}
def __init__(self, roots: list[Path], debug=False):
self.roots = roots
self.debug = debug
def _render_index_html(
self, cur_root: Path, root: Path, dirs: list[str], files: list[str]
) -> str:
"""Render the index html."""
dir_infos = [DirInfo.from_dir(cur_root / sub_dir) for sub_dir in sorted(dirs)]
file_infos = [FileInfo.from_dir(cur_root / file) for file in sorted(files)]
template = self.template_env.get_template("dirlisting.html.j2")
breadcrumbs = cur_root.relative_to(root).parts
breadcrumbs_with_links = [
(bc, "../" * (len(breadcrumbs) - i - 1)) for i, bc in enumerate(breadcrumbs)
]
stylesheet_url = "../" * len(breadcrumbs) + DIRLISTING_STYLE_NAME
return template.render(
{
"dirs": dir_infos,
"files": file_infos,
"dirname": cur_root.name,
"breadcrumbs": breadcrumbs_with_links,
"stylesheet_url": stylesheet_url,
}
)
def _write_index_file(
self,
cur_root: Path,
root: Path,
dirs: list[str],
files: list[str],
spinner: YaspinWrapper,
) -> None:
"""Create the index file."""
index_html = self._render_index_html(cur_root, root, dirs, files)
index_file_name = cur_root / "index.html"
with index_file_name.open("w") as index_file:
index_file.write(index_html)
spinner.write(f"{index_file_name} written.")
def create_dirlisting_in_root(self, root: Path, spinner: YaspinWrapper):
"""docstring for create_dirlisting_in_root"""
shutil.copy(STYLESHEET_PATH, root / DIRLISTING_STYLE_NAME)
for cur_root, dirs, files in os.walk(root):
dirs[:] = [path for path in dirs if not path.startswith(".")]
files[:] = [
file
for file in files
if not file.startswith(".") and not file in self.IGNORE_FILES
]
# render and write index.html
self._write_index_file(
cur_root=Path(cur_root),
root=root,
dirs=dirs,
files=files,
spinner=spinner,
)
def run(self):
"""Run the command."""
with YaspinWrapper(
debug=self.debug, color="cyan", text="Rendering..."
) as spinner:
for i, root in enumerate(self.roots):
progress_str = (
f"{i + 1:{len(str(len(self.roots)))}} / {len(self.roots)}"
)
spinner.text = f"[{progress_str}] rendering in {root}."
self.create_dirlisting_in_root(root, spinner=spinner)
def main():
args = parse_args()
cli = Cli(roots=args.roots, debug=args.debug)
cli.run()
if __name__ == "__main__":
main()
......@@ -153,6 +153,20 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"]
plugins = ["setuptools"]
[[package]]
name = "jinja2"
version = "3.0.1"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "kiwisolver"
version = "1.3.2"
......@@ -183,6 +197,14 @@ html5 = ["html5lib"]
htmlsoup = ["beautifulsoup4"]
source = ["Cython (>=0.29.7)"]
[[package]]
name = "markupsafe"
version = "2.0.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "matplotlib"
version = "3.4.3"
......@@ -463,7 +485,7 @@ termcolor = ">=1.1.0,<2.0.0"
[metadata]
lock-version = "1.1"
python-versions = ">=3.9,<3.11"
content-hash = "3df3d2b86614dbf3530b64458e1fd4d8e7c8170d08f6f2f4f158227d6e4673b4"
content-hash = "2ded71ac90683d8f0dfc0800a2b74a4f8d9732b33cf51b1208fabd7bbdf379f1"
[metadata.files]
appdirs = [
......@@ -521,6 +543,10 @@ isort = [
{file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"},
{file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"},
]
jinja2 = [
{file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"},
{file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"},
]
kiwisolver = [
{file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1d819553730d3c2724582124aee8a03c846ec4362ded1034c16fb3ef309264e6"},
{file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d93a1095f83e908fc253f2fb569c2711414c0bfd451cab580466465b235b470"},
......@@ -629,6 +655,42 @@ lxml = [
{file = "lxml-4.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83"},
{file = "lxml-4.6.3.tar.gz", hash = "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468"},
]
markupsafe = [
{file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
]
matplotlib = [
{file = "matplotlib-3.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c988bb43414c7c2b0a31bd5187b4d27fd625c080371b463a6d422047df78913"},
{file = "matplotlib-3.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f1c5efc278d996af8a251b2ce0b07bbeccb821f25c8c9846bdcb00ffc7f158aa"},
......
......@@ -15,6 +15,7 @@ termcolor = "^1.1.0"
numpy = "^1.21.2"
yaspin = "^2.1.0"
humanize = "^3.11.0"
Jinja2 = "^3.0.1"
[tool.poetry.dev-dependencies]
pylint = "^2.6.0"
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index of "{{ dirname }}"</title>
<link rel="stylesheet" href="{{ stylesheet_url }}">
</head>
<body>
<div class="container">
<div class="row mt-5">
<h1>Index of "{{ dirname }}"</h1>
</div>
<div class="row mt-5">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{% for bc in breadcrumbs %}
<li class="breadcrumb-item{% if loop.last %} active{% endif %}">
<a href="{{ bc[1] }}index.html">{{ bc[0] }}</a>
</li>
{% endfor %}
</ol>
</nav>
</div>
<div class="row">
<table class="table table-striped table-hover table-sm text-monospace">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Modification Date</th>
<th>Size</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td><a href="../index.html">Go up</a></td>
<td></td>
<td></td>
</tr>
{% for dir in dirs %}
<tr>
<td>🗀</td>
<td>
<a href="{{ dir.path.name }}/index.html">
{{ dir.path.name }}
</a>
</td>
<td>{{ dir.date.strftime('%Y-%m-%d %H:%M') }}</td>
<td>-</td>
</tr>
{% endfor %}
{% for file in files %}
<tr>
<td>🗎</td>
<td>
<a href="{{ file.path.name }}">
{{ file.path.name }}
</a>
</td>
<td>{{ file.date.strftime('%Y-%m-%d %H:%M') }}</td>
<td>{{ file.size|filesizeformat(binary=True) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not files and not dirs %}
<div class="alert alert-danger">
🛇 No file or directory
</div>
{% endif %}
</div>
</div>
</body>
</html>
*, ::after, ::before {
box-sizing: border-box;
}
body, td, tfoot, th, thead, tr {
border-color: inherit;
border-style: solid;
border-width: 0;
}
.text-monospace {
font-family: monospace;
}
.breadcrumb {
display: flex;
flex-wrap: wrap;
padding: 0 0;
margin-bottom: 1rem;
list-style: none;
}
.breadcrumb-item.active {
color: #6c757d;
}
.breadcrumb-item + .breadcrumb-item {
padding-left: .5rem;
}
.breadcrumb-item + .breadcrumb-item::before {
float: left;
padding-right: .5rem;
color: #6c757d;
content: "/";
}
.container {
max-width: 960px;
width: 100%;
padding-right: var(--bs-gutter-x,.75rem);
padding-left: var(--bs-gutter-x,.75rem);
margin-right: auto;
margin-left: auto;
}
.table {
width: 100%;
margin-bottom: 1rem;
color: #000;
vertical-align: middle;
border-color: #000;
}
.table > :not(caption) > * > * {
padding: .5rem .5rem;
background-color: transparent;
border-bottom-width: 1px;
box-shadow: inset 0 0 0 9999px transparent;
}
.table > tbody {
vertical-align: inherit;
}
.table > thead {
vertical-align: bottom;
}
.table > :not(:last-child) > :last-child > * {
border-bottom-color: rgba(0,0,0,0.1);
}
.table > tbody > tr:nth-of-type(2n+1) {
background-color: rgba(0,0,0,0.01);
color: #252519;
}
.table-hover > tbody > tr:hover {
background-color: rgba(0,0,0,0.05);
color: #252519;
}
.table-bordered > :not(caption) > * {
border-width: 1px 0;
}
.table-bordered > :not(caption) > * > * {
border-width: 0 1px;
}
.alert {
text-align: center;
border-radius: 0.5rem;
padding: 0.5rem;
}
.alert.alert-danger {
background-color: #fcc;
border: 1px solid #faa;
}
......@@ -227,6 +227,24 @@ def create_relpath(path1: Path, path2: Optional[Path] = None) -> Path:
return Path(os.path.relpath(path1, common_prefix))
def existing_dir_path(value: str, allow_none=False) -> Optional[Path]:
if not value or value.lower() == "none":
if allow_none:
return None
else:
raise argparse.ArgumentTypeError("`none` is not allowed here.")
path = Path(value)
if path.is_file():
raise argparse.ArgumentTypeError(f"{value} is a file. A directory is required.")
elif not path.is_dir():
raise argparse.ArgumentTypeError(f"{value} does not exist.")
return path
def existing_file_path(value: str, allow_none=False) -> Optional[Path]:
if not value or value.lower() == "none":
if allow_none:
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment