diff --git a/.gitignore b/.gitignore index 55be276..75d38aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +config.py + # ---> Python # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/README.md b/README.md index 7e4ad83..3f698ff 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # landoc -Server for sharing documents over a LAN \ No newline at end of file +Server for sharing documents over a LAN + +Not well-tested; please do not use this over the Internet diff --git a/config.py b/config.py new file mode 100644 index 0000000..9bd74e4 --- /dev/null +++ b/config.py @@ -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 diff --git a/landoc.py b/landoc.py new file mode 100644 index 0000000..99bfd6f --- /dev/null +++ b/landoc.py @@ -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"{title}: {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'
  • {name}
  • \n' + for name, path in path_segments[:-1] + ] + ) + + f"
  • {path_segments[-1][0]}
  • " + ) + actions = ( + """
    + """ + if show_print + else "" + ) + + body = ( + f""" + + + {path} - {title} + + + + + +
    + + {actions} +
    +
    +{main} +
    + + +""", + ) + 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"
    {bleach.clean(text)}
    ") + + +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'
  • {i}{"/" if os.path.isdir(os.path.join(root, path[1:], i)) else ""}
  • ' + for i in contents + ] + ) + return render( + path, f"", show_print=False + ) + else: + return flask.redirect(path + "/") + + +renderers = {"txt": render_txt, "md": render_md} + + +@app.route("/") +@app.route("/") +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"

    403 Forbidden

    {path}

    Disallowed file type: {file_type}", + 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"

    404 Not Found

    {path}

    ", + show_print=False, + status_code=404, + )