Redbean Tiddlywiki Saver
Tiddlywiki is a great tool, but because it runs in the browser, it has only limited access to the filesystem to save changes. This has led to dozens of tools called "savers" that work with Tiddlywiki to save it. These span the gamut, including full fledged desktop apps (most notably TiddlyDesktop), browser extensions, and server applications of varying complexity. But all of these rely on a single capability built-in to Tiddlywiki that allows it to save itself to any WebDAV server. In addition to purpose-built servers, Tiddlywiki can save itself to WebDAV servers available for:
I started to consider writing a tiny version of a WebDAV tiddlywiki saver after I ran across jart's phenomenal Redbean project, which is a webserver contained as an αcτµαlly pδrταblε εxεcµταblε. This approach appeals strongly to my love of small, self-contained tools (one of the reasons I love Tiddlywiki!) It's a web server application that is also a zip archive. By adding Lua code and assets to the zip archive, and then executing it on most any x86_64 system, it provides a self-contained, offline web application. After reading through the documentation for Redbean, it occurred to me that it should be not only possible, but quite easy to write a Redbean application that not only is a saver for Tiddlywiki, but can bundle the wiki into the application itself, creating a completely self-contained Tiddlywiki application.
But, I've never worked with Redbean before, so these are my notes on implementing this idea.
Redbean's Hello World
Here's my hello world in
Write("<html>") Write("<head><title>Redbean Lua Demo</title></head>") Write("<body>") Write("This is just a demo page...hello, world!") Write("</body>") Write("</html>")
I then build the executable using a small script:
if [! -f redbean.com ]; then curl https://redbean.dev/redbean-latest.com > redbean.com chmod +x redbean.com fi cp redbean.com wiki.com zip wiki.com index.lua ./wiki.com
Serving a Tiddlywiki
Instead of generating HTML in the Lua code, we already have a bundle of HTML to serve: a Tiddlywiki! This means all the Lua code has to do is serve that asset:
Of course, we need to provide that
wiki.html file to serve. Let's grab one:
curl https://tiddlywiki.com/empty.html > wiki.html zip wiki.com index.lua wiki.html ./wiki.com
localhost:8080 in the browser, we can see that the wiki loads, and can be edited, but there's no saving yet.
Redbean has a surprisingly comprehensive set of methods available to tackle the saving piece. In order to enable saving for Tiddlywiki with the built-in DAV tooling, the server needs to understand four major HTTP methods:
HEAD, we want to provide two relevant headers: the
Content-Type and the
Content-Length. Right now, the goal is to have Redbean store exactly one wiki, so the path to the resource doesn't matter. But since we'll have to dispatch on method type, we need to extract that first. At the same time, we'll hardcode the path to the wiki inside the zip:
method = GetMethod() WIKI_PATH="wiki.html"
GET we simply use the logic from above:
if method == "GET" then ServeAsset(WIKI_PATH) end
HEAD, two headers are required, and they can be set with
SetHeader. One is
Content-Length, which is dynamic, so it first needs to load the wiki and get its length before setting the headers:
-- GET method above elseif method == "HEAD" then wiki = LoadAsset(WIKI_PATH) SetStatus(200) SetHeader("Content-Type", "text/html") SetHeader("Content-Length", tostring(#wiki)) end
OPTIONS has to emulate WebDAV just enough to trick Tiddlywiki into saving using that mechanism. I based these headers from the very sleek Ruby saver written by Jim Foltz.
-- GET and HEAD methods above elseif method == "OPTIONS" then SetStatus(200) SetHeader("allow", "GET,HEAD,POST,OPTIONS,CONNECT,PUT,DAV,dav") SetHeader("x-api-access-type", "file") SetHeader("dav", "tw5/put") end
The last method is
PUT, which needs to write the payload back to the Redbean archive. Luckily, Redbean provides a
StoreAsset function for exactly this case:
-- GET, HEAD, and OPTIONS methods above elseif method == "PUT" then length = tonumber(GetHeader("Content-Length")) body = GetBody() StoreAsset(WIKI_PATH, body) SetStatus(200) end
Putting It All Together
So, the full script is a compact 21 lines of Lua:
method = GetMethod() WIKI_PATH="wiki.html" if method == "GET" then ServeAsset(WIKI_PATH) elseif method == "HEAD" then wiki = LoadAsset(WIKI_PATH) SetStatus(200) SetHeader("Content-Type", "text/html") SetHeader("Content-Length", tostring(#wiki)) elseif method == "OPTIONS" then SetStatus(200) SetHeader("allow", "GET,HEAD,POST,OPTIONS,CONNECT,PUT,DAV,dav") SetHeader("x-api-access-type", "file") SetHeader("dav", "tw5/put") elseif method == "PUT" then length = tonumber(GetHeader("Content-Length")) body = GetBody() StoreAsset(WIKI_PATH, body) SetStatus(200) end
Then all that's needed is to inject
index.lua, along with a wiki, into the archive and run it:
zip wiki.com wiki.html index.lua ./wiki.com
While this loads and allows editing as before, upon saving, Tiddlywiki throws an error, and the Redbean logs reveal a
413 error: the payload during the
PUT is too large. This is because a Tiddlywiki is over 1MB, but the maximum payload size of Redbean is 65k. This can be adjusted during launch...here we'll use 5MB:
./wiki.com -M 5000000
This approach allows Tiddlywiki to load and save exactly as expected, and packs everything into a single 2.5M file.
- 2022-11-19 - Added details about WebDAV saving mechanism after feedback on HN.