Skip to content

Instantly share code, notes, and snippets.

@jimfoltz
Last active July 4, 2025 12:46
Show Gist options
  • Save jimfoltz/ee791c1bdd30ce137bc23cce826096da to your computer and use it in GitHub Desktop.
Save jimfoltz/ee791c1bdd30ce137bc23cce826096da to your computer and use it in GitHub Desktop.
A local server for TiddlyWiki5 that allows saving wiki.
require 'webrick'
require 'fileutils'
BIND_ADDRESS = "127.0.0.1"
PORT = 8080
BACKUP_DIR = 'bak'
if ARGV.length != 0
root = ARGV.first.gsub('\\', '/')
else
root = '.'
end
module WEBrick
module HTTPServlet
class FileHandler
alias do_PUT do_GET
end
class DefaultFileHandler
def do_PUT(req, res)
file = "#{@config[:DocumentRoot]}#{req.path}"
res.body = ''
unless Dir.exist? BACKUP_DIR
Dir.mkdir BACKUP_DIR
end
FileUtils.cp(file, "#{BACKUP_DIR}/#{File.basename(file, '.html')}.#{Time.now.to_i.to_s}.html")
File.open(file, "w+") {|f| f.puts(req.body)}
end
def do_OPTIONS(req, res)
res['Allow'] = "GET,HEAD,OPTIONS,PUT"
res['dav'] = 'anything' # TW looks for a 'dav' header, and ignores any value
end
end
end
end
server = WEBrick::HTTPServer.new({:Port => PORT, :DocumentRoot => root, :BindAddress => BIND_ADDRESS})
# ctrl-c handler
trap "INT" do
puts "Shutting down..."
server.shutdown
end
puts "Serving on http://#{BIND_ADDRESS}:#{PORT}"
server.start
@brianemery
Copy link

brianemery commented Aug 14, 2021

Hi Jim, I suggest adding some code to specify the bind address. If I am not mistaken, this will prevent webbrick from accepting connections from remote hosts (a portscan with nmap suggests this is true), and exposing your file system to the internet:

server = WEBrick::HTTPServer.new({:Port => 8000, :DocumentRoot => root, :BindAddress => "127.0.0.1"})

I've also added some notes on how to run this (See link).
Otherwise, thanks for this -Brian

https://github.com/brianemery/tw5_server/blob/main/tw5-server.rb

@jimfoltz
Copy link
Author

Hi Brian - good idea, thanks.

(Your link isn't working.)

@jimfoltz
Copy link
Author

Should also maybe limit the backups in some sort of rotation, or every X number of saves, or something.

@brianemery
Copy link

Ok, I think the link is fixed now -Brian

@korikori
Copy link

Hi Jim and Brian, since this isn't a repository and I can't open pull requests - just wanted to let you know that I've fixed a trailing slash bug (that may only affect my use case), and I've reformatted and expanded on the setup instructions at https://github.com/korikori/tw5_server.

@jimfoltz
Copy link
Author

@korikori - Cool, thanks. This is something I wrote, used for a minute (until I started using Timini), and haven't thought about since.

If you want, make a pull request at the TiddlyWiki repo to have the official docs point to your repo - I'll endorse it.

@korikori
Copy link

Thanks @jimfoltz, I really don't think it's necessary to make any further changes to the TiddlyWiki page - I believe that the discussion here is sufficient for anyone else who may need more information on how to run this behind NGINX as proxy.

@DestyNova
Copy link

DestyNova commented Mar 22, 2022

Should also maybe limit the backups in some sort of rotation, or every X number of saves, or something.

Hi @jimfoltz -- I've made a fork with a small modification to rotate between a fixed number of backups (defined by the BACKUP_VERSIONS constant). There's probably a much better way to do it but my Ruby is really weak. 😅
Thanks for making this BTW, really useful.

@chryoung
Copy link

chryoung commented Apr 7, 2022

Thank you for making this simple server script. I've modified it a little bit so it accepts the options for binding address and port.

tiddlywiki_server.rb

@ScottJWhite
Copy link

Works!

By the way for anyone else that wants to run this in a docker container. I had to change the BindAddress to get this to be accessible outside the contianer.

Change to:

:BindAddress => "0.0.0.0"}

@huataihuang
Copy link

Dir.exists? was deprecated in Ruby 2.1.0 and has been removed in the Ruby 3.2.0

https://www.reddit.com/r/ruby/comments/1196wti/psa_and_a_little_rant_fileexists_direxists/

suggest change:

            unless Dir.exist? BACKUP_DIR
               Dir.mkdir BACKUP_DIR
            end

@ClimaxUke
Copy link

Hello,
I've noticed this error. Any ideas?
Thanks in advance.

[2024-01-11 19:27:50] ERROR NoMethodError: undefined method `exists?' for Dir:Class
        tw5-server.rb:22:in `do_PUT'
        /usr/lib/ruby/gems/3.2.0/gems/webrick-1.8.1/lib/webrick/httpservlet/abstract.rb:105:in `service'
        /usr/lib/ruby/gems/3.2.0/gems/webrick-1.8.1/lib/webrick/httpservlet/filehandler.rb:315:in `exec_handler'
        /usr/lib/ruby/gems/3.2.0/gems/webrick-1.8.1/lib/webrick/httpservlet/filehandler.rb:246:in `do_GET'
        /usr/lib/ruby/gems/3.2.0/gems/webrick-1.8.1/lib/webrick/httpservlet/abstract.rb:105:in `service'
        /usr/lib/ruby/gems/3.2.0/gems/webrick-1.8.1/lib/webrick/httpservlet/filehandler.rb:242:in `service'
        /usr/lib/ruby/gems/3.2.0/gems/webrick-1.8.1/lib/webrick/httpserver.rb:140:in `service'
        /usr/lib/ruby/gems/3.2.0/gems/webrick-1.8.1/lib/webrick/httpserver.rb:96:in `run'
        /usr/lib/ruby/gems/3.2.0/gems/webrick-1.8.1/lib/webrick/server.rb:310:in `block in start_thread'

with

 ruby --version
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-linux-musl]

@jimfoltz
Copy link
Author

jimfoltz commented Jan 12, 2024

@ClimaxUke

Use Dir.exist? instead.

@huataihuang - thank you for the suggestion and fix.

@barrettondricka
Copy link

May I suggest printing the url the user needs to open? It is more convenient, and less confusing for newer users.

BIND_ADDRESS = "127.0.0.1" # localhost
PORT = 8000

server = WEBrick::HTTPServer.new({:Port => PORT, :DocumentRoot => root, :BindAddress => BIND_ADDRESS})

puts "Serving on http://#{BIND_ADDRESS}:#{PORT}"

@jimfoltz
Copy link
Author

May I suggest printing the url the user needs to open? It is more convenient, and less confusing for newer users.

BIND_ADDRESS = "127.0.0.1" # localhost
PORT = 8000

server = WEBrick::HTTPServer.new({:Port => PORT, :DocumentRoot => root, :BindAddress => BIND_ADDRESS})

puts "Serving on http://#{BIND_ADDRESS}:#{PORT}"

Done - thanks.

@barrettondricka
Copy link

barrettondricka commented Jun 19, 2025

Backups are heavy on storage, because empty wikis are 2.4MB and they backup after every small edit. With heavy usage you get to a GB in about a month.

My solutions is to backup randomly every ~5th save. Of course you could use a simple counter, but I trust my luck more.

RANDOM_BACKUP_PERIOD = 5

module WEBrick
   module HTTPServlet
      class DefaultFileHandler
         def do_PUT(req, res)
            # ...
            if rand(0...RANDOM_BACKUP_PERIOD).zero? # backup
              FileUtils.cp(file, "#{BACKUP_DIR}/#{File.basename(file, '.html')}.#{Time.now.to_i.to_s}.html")
            end
            # ...
# ...

I will just leave it here for anyone who needs it, no pressure to the maintainer.

@jimfoltz
Copy link
Author

jimfoltz commented Jul 4, 2025

Backups are heavy on storage, because empty wikis are 2.4MB and they backup after every small edit. With heavy usage you get to a GB in about a month.

Thanks and I agree. There are too many backup strategies to included here. At some point maybe this should be moved to a repo so contributions can be properly considered.

I have an idea to write a server that uses the put saver to save the entire file, but then parses the json store and saves each tiddler individually. That might make sense if users want to use git as a backup and history strategy.

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