Skip to content

Instantly share code, notes, and snippets.

@FND
Last active January 23, 2021 09:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save FND/e765a0da6be27fb8b9e1779ae7e3ef4b to your computer and use it in GitHub Desktop.
Save FND/e765a0da6be27fb8b9e1779ae7e3ef4b to your computer and use it in GitHub Desktop.
minimal WebDAV implementation for TiddlyWiki
#!/usr/bin/env python3
import os
from hashlib import md5
ROOT_DIR = os.path.dirname(__file__)
STATUSES = {
200: "OK",
204: "No Content",
404: "Not Found",
405: "Method Not Allowed",
412: "Precondition Failed",
415: "Unsupported Media Type"
}
def handler(environ, start_response):
method = environ["REQUEST_METHOD"]
respond = lambda *args, **kwargs: make_response(start_response, *args, **kwargs)
if method == "OPTIONS":
return respond(200, [("DAV", "1")]) # XXX: TiddlyWiki expects 200 rather than 204
if method == "HEAD":
filepath = _determine_filepath(environ)
return respond(204, headers=[
("ETag", _determine_etag(filepath))
])
elif method == "GET":
uri = environ.get("PATH_INFO", "")
if uri == "/favicon.ico": # TODO: read from disk
return respond(404)
else:
return serve_file(environ, start_response)
if method == "PUT":
return store_file(environ, start_response)
else:
return respond(405, body="invalid request")
def serve_file(environ, start_response):
filepath = _determine_filepath(environ)
with open(filepath) as fh:
return make_response(start_response, 200,
headers=[("Content-Type", "text/html")], # NB: TiddlyWiki-specific
body=fh.read()) # XXX: inefficient
def store_file(environ, start_response):
respond = lambda *args, **kwargs: make_response(start_response, *args, **kwargs)
if environ["CONTENT_TYPE"] != "text/html;charset=UTF-8": # NB: TiddlyWiki-specific heuristic
return respond(415)
filepath = _determine_filepath(environ)
etag = environ.get("HTTP_IF_MATCH")
if etag is not None:
_hash = _determine_etag(filepath)
if etag != _hash:
return respond(412, body=("expected ETag %s, received %s" % (_hash, etag)))
try:
size = int(environ["CONTENT_LENGTH"])
content = environ["wsgi.input"].read(size)
except (KeyError, ValueError):
return make_response(400, body="missing `Content-Length` request header")
with open(filepath, "w") as fh:
fh.write(content.decode("utf-8"))
return respond(204)
def make_response(start_response, status, headers=[], body=[]):
status_line = "%s %s" % (status, STATUSES[status])
start_response(status_line, headers)
if isinstance(body, str): # XXX: simplistic
return [body.encode("utf-8")]
return body
def _determine_etag(filepath):
with open(filepath) as fh:
content = fh.read()
return md5(content.encode("utf-8")).hexdigest()
def _determine_filepath(environ):
uri = environ.get("PATH_INFO", "")
filename = uri[1:] if uri.startswith("/") else uri
if len(filename) == 0:
filename = "index.html" # NB: TiddlyWki-specific
return os.path.abspath(os.path.join(ROOT_DIR, filename))
if __name__ == "__main__":
from wsgiref.simple_server import make_server
host = "localhost"
port = 8080
srv = make_server(host, port, handler)
print("→ http://%s:%s" % (host, port))
srv.serve_forever()
@Dialga
Copy link

Dialga commented Jan 23, 2021

This script will not work if placed in /local/bin, it will search for index.html there rather than the cwd.

@FND
Copy link
Author

FND commented Jan 23, 2021

Hey @Dialga, I didn't expect anyone but myself to use this, but you could change line 8 to make that work:

-ROOT_DIR = os.path.dirname(__file__)
+ROOT_DIR = os.getcwd()

How did you come across this script in the first place?

Note that there might be security risks if others can access it via the web because modifying the URL might allow targeting arbitrary files on your disk (see line 94).

@Dialga
Copy link

Dialga commented Jan 23, 2021

I searched for it on gist.
I was looking for a tiddlywiki server that behaves like the nodejs version, which saves individual tiddlers, rather than saving the whole file each time.
If this script is in the cwd then it would be accessible at localhost:8080/webdav.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment