File size: 4,724 Bytes
81ed537 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | #!/usr/bin/env python3
"""Local dev server for the Klein ML blog, with an in-browser editor for posts.
python3 dev/server.py # http://127.0.0.1:8000
python3 dev/server.py --port 8001 # another port
Serves the site like `python3 -m http.server`. HTML pages get dev/editor.js added on the way out, so the
files on disk stay clean. Open a post, click "Edit page", change the title, byline or article text, then
"Save" (or Ctrl+S). Saving rewrites the title/byline and article sections of that .html file.
Localhost only. Nothing here is used by the published Hugging Face Space.
"""
import argparse
import json
import re
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
HERE = Path(__file__).resolve().parent
EDITOR = HERE / "editor.js"
INJECT = b'<script src="/__dev/editor.js" defer></script>\n</body>'
# Regions the editor may rewrite. Everything else in the file is left as it is.
BAND_RE = re.compile(r'(<div class="band">)(.*?)(</div>\s*<figure)', re.S)
ARTICLE_RE = re.compile(r'(<article class="article">)(.*?)(</article>)', re.S)
class Handler(SimpleHTTPRequestHandler):
root = HERE.parent # replaced in main()
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=str(Handler.root), **kwargs)
def _send(self, code, body, ctype):
self.send_response(code)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
def _json(self, code, obj):
self._send(code, json.dumps(obj).encode("utf-8"), "application/json; charset=utf-8")
def do_GET(self):
path = self.path.split("?", 1)[0]
if path == "/__dev/editor.js":
return self._send(200, EDITOR.read_bytes(), "text/javascript; charset=utf-8")
target = Path(self.translate_path(path))
if target.is_dir():
target = target / "index.html"
if target.suffix == ".html" and target.is_file():
html = target.read_bytes()
html = html.replace(b"</body>", INJECT, 1) if b"</body>" in html else html + INJECT
return self._send(200, html, "text/html; charset=utf-8")
return super().do_GET()
def do_POST(self):
if self.path != "/__dev/save":
return self._json(404, {"error": "not found"})
# A custom header and a JSON body make the browser check cross-site requests first,
# so other sites can't write to your files through this server.
if self.headers.get("X-Dev-Editor") != "1" or "application/json" not in (self.headers.get("Content-Type") or ""):
return self._json(400, {"error": "bad request"})
try:
size = int(self.headers.get("Content-Length", "0"))
data = json.loads(self.rfile.read(size) or b"{}")
root = Handler.root.resolve()
page = (root / str(data["page"]).lstrip("/")).resolve()
if not page.is_relative_to(root) or page.suffix != ".html" or not page.is_file():
raise ValueError("not a page in this site")
text = page.read_text(encoding="utf-8")
if "band" in data:
text, n = BAND_RE.subn(lambda m: m.group(1) + "\n " + str(data["band"]).strip() + "\n" + m.group(3), text, count=1)
if n == 0:
raise ValueError("this page has no title block to save into")
if "article" in data:
text, n = ARTICLE_RE.subn(lambda m: m.group(1) + "\n" + str(data["article"]).strip() + "\n " + m.group(3), text, count=1)
if n == 0:
raise ValueError("this page has no article to save into")
page.write_text(text, encoding="utf-8")
return self._json(200, {"ok": True, "page": str(page.relative_to(root))})
except (KeyError, ValueError, OSError, json.JSONDecodeError) as e:
return self._json(400, {"error": str(e)})
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--port", type=int, default=8000)
ap.add_argument("--root", default=str(HERE.parent), help="site folder (default: the repo)")
args = ap.parse_args()
Handler.root = Path(args.root).resolve()
srv = ThreadingHTTPServer(("127.0.0.1", args.port), Handler)
print(f"Serving {Handler.root} at http://127.0.0.1:{args.port}")
print("Open a post and click 'Edit page'. Ctrl+S or 'Save' writes it back to the .html file.")
srv.serve_forever()
if __name__ == "__main__":
main()
|