initial commit

This commit is contained in:
Samuel Sloniker 2022-04-22 20:06:35 -07:00
parent 269799c80f
commit d10add04ff
4 changed files with 290 additions and 1 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
config.py
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/

View File

@ -1,3 +1,5 @@
# landoc
Server for sharing documents over a LAN
Not well-tested; please do not use this over the Internet

15
config.py Normal file
View File

@ -0,0 +1,15 @@
root = "/path/to/documents/"
# The path to the direvtory containing the documents
title = "User-Visible Title"
# A user-visible title for the server (e.g. "School Documents" or "My Notes")
allowed_file_types = ["txt", "odt", "pdf", "md"]
# File types that LANdoc will serve; all other file types will result in 403
# Forbidden
# To disallow one of these file types, remove it; to allow another type, add
# its extension without the `.`
# `md` and `txt` files will be displayed in the LANdoc viewer; all other files
# will be sent as-is to the client

270
landoc.py Normal file
View File

@ -0,0 +1,270 @@
import flask
import jinja2
import os
import mimetypes
import bleach
from markdown_it import MarkdownIt
app = flask.Flask(__name__)
md = MarkdownIt("commonmark", {"typographer": True}).enable(
["replacements", "smartquotes"]
)
def split_url(url):
segments = [""]
for character in url:
segments[-1] += character
if character == "/":
segments.append("")
if not segments[-1]:
del segments[-1]
paths = [
[segment, "".join(segments[: i + 1])]
for i, segment in enumerate(segments)
]
paths[0][0] = f"<strong>{title}</strong>: {paths[0][0]}"
return paths
def render(path, main, show_print=True, status_code=200):
path = jinja2.filters.escape(path)
path_segments = split_url(path)
path_html = (
"".join(
[
f' <li class=crumb><a href="{path}" class=button>{name}</a></li>\n'
for name, path in path_segments[:-1]
]
)
+ f" <li class=crumb><span class=button>{path_segments[-1][0]}</span></li>"
)
actions = (
"""<hr>
<ul class=buttons>
<li class=crumb>
<a class=button href="javascript:void print();">Print</a>
</li>
</ul>"""
if show_print
else ""
)
body = (
f"""<!DOCTYPE html>
<html>
<head>
<title>{path} - {title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<style>
header {{
border: 1px solid black;
padding: 5px;
background-color: #F0F0F0;
border-radius: 20px;
}}
a.button, span.button {{
border: 1px solid #404040;
background-color: white;
border-radius: 100px;
padding: 8px;
margin: 3px;
min-width: 10px;
display: inline-block;
}}
a.button {{
color: blue;
text-decoration: none;
}}
* {{
font-family: "DejaVu Sans", "DejaVu Sans Book", "DejaVu Sans Regular", "Noto Sans", "Noto Sans Regular", sans-serif;
}}
pre {{
font-family: monospace;
}}
ul.buttons {{
display: inline;
padding: 0;
}}
li.crumb {{
display: inline;
}}
button[disabled] {{
color: black;
}}
main {{
margin: 1em;
max-width: 40em;
}}
p {{
line-height: 200%;
text-indent: 0.5in;
}}
a.direntry {{
color: blue !important;
}}
@media print {{
* {{
font-family: "DejaVu Serif", "DejaVu Serif Book", "DejaVu Serif Regular", "Noto Serif", "Noto Serif Regular", serif;
}}
header {{
display: none;
}}
main {{
max-width: 1000em;
}}
h1, h2, h3, h4, h5, h6 {{
break-after: avoid;
}}
}}
</style>
</head>
<body>
<header>
<nav>
<ul class=buttons>
{path_html}
</ul>
</nav>
{actions}
<noscript>
<hr>
<p><strong>Note:</strong> JavaScript appears to be disabled. Some features, such as printing, may not work.<p>
</noscript>
</header>
<main>
{main}
</main>
</body>
</html>
""",
)
return flask.Response(
body,
headers={"Content-Type": "text/html"},
status=status_code,
)
def render_txt(text, path):
text = jinja2.filters.escape(text.decode("utf-8"))
return render(path, f"<pre>{bleach.clean(text)}</pre>")
def render_md(text, path):
text = bleach.clean(
md.render(text.decode("utf-8")),
tags=[
"a",
"abbr",
"acronym",
"b",
"blockquote",
"code",
"em",
"i",
"li",
"ol",
"strong",
"ul",
"p",
"pre",
"img",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"hr",
],
attributes={
"a": ["href", "title"],
"abbr": ["title"],
"acronym": ["title"],
"ol": ["start"],
"li": ["value"],
},
)
return render(path, text)
def render_directory(contents, path):
# TODO: strikethrough forbidden files
if path.endswith("/"):
contents.sort()
listing = "".join(
[
f'<li><a href="{os.path.join(path, i)}" class=direntry>{i}{"/" if os.path.isdir(os.path.join(root, path[1:], i)) else ""}</a></li>'
for i in contents
]
)
return render(
path, f"<ul class=listing>{listing}</ul>", show_print=False
)
else:
return flask.redirect(path + "/")
renderers = {"txt": render_txt, "md": render_md}
@app.route("/")
@app.route("/<path:path>")
def main(path=""):
# TODO: remove // and fix ..
full_path = os.path.join(root, path)
path = "/" + path
file_type = path.split(".")[-1]
try:
return render_directory(os.listdir(full_path), path)
except NotADirectoryError:
if not file_type in allowed_file_types:
return render(
path,
f"<h1>403 Forbidden</h1><p>{path}</p><p>Disallowed file type: <code>{file_type}</code>",
show_print=False,
status_code=403,
)
elif file_type in renderers:
with open(full_path, "rb") as f:
return renderers[file_type](f.read(), path)
else:
if path.endswith("/"):
return flask.redirect(path[:-1])
else:
with open(full_path, "rb") as f:
return flask.Response(
f.read(),
headers={
"Content-Type": mimetypes.guess_type(full_path)[0]
},
)
except FileNotFoundError:
return render(
path,
f"<h1>404 Not Found</h1><p>{path}</p>",
show_print=False,
status_code=404,
)