Skip to content

Instantly share code, notes, and snippets.

@mwgamera
Last active October 12, 2019 16:43
Show Gist options
  • Save mwgamera/cf6ccda7eee305fe58b9e942f7ce009e to your computer and use it in GitHub Desktop.
Save mwgamera/cf6ccda7eee305fe58b9e942f7ce009e to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
# Like <https://gist.github.com/jimfoltz/ee791c1bdd30ce137bc23cce826096da>
# but cooler. Keep only 10 last revisions, gzipped.
# klg, Jun 2019
require 'securerandom'
require 'webrick'
require 'zlib'
class Tw5 < WEBrick::HTTPServlet::AbstractServlet
def do_OPTIONS req, res
res['allow'] = 'GET,HEAD,OPTIONS,PUT'
res['dav'] = 'tw5/put' # has DAV but not compatible
end
def do_GET req, res
f = File.open "./#{req.path}.gz"
st = f.stat
make_validators res, st
check_validators req, res
res['content-type'] = 'text/html;charset=utf-8'
if a = req['accept-encoding'] and
WEBrick::HTTPUtils.parse_qvalues(a).member?('gzip')
res['content-encoding'] = 'gzip'
res['content-length'] = st.size
res.body = f
else
Zlib::GzipReader.open f do |gz|
res.body = gz.read
end
f.close
end
res.status = 200
rescue Errno::ENOENT
begin
f = File.open "./#{req.path}"
st = f.stat
unless st.file?
raise WEBrick::HTTPStatus::NotFound.new "#{req.path} not found."
end
make_validators res, st
check_validators req, res
res['content-length'] = st.size
res.body = f
res.status = 200
rescue Errno::ENOENT
raise WEBrick::HTTPStatus::NotFound.new "#{req.path} not found."
end
end
def do_PUT req, res
if req['content-range'] || req['content-encoding']
raise WEBrick::HTTPStatus::NotImplemented
end
path = "./#{req.path}.gz".gsub /\/\.*(?=\/|$)/, ''
tmpn = "./tmp.#{$$}-#{SecureRandom.hex(6)}"
Zlib::GzipWriter.open tmpn do |gz|
req.body { |chunk| gz.write chunk }
end
res.status = 204
rev = 0
File.open '.' do |dir|
dir.flock File::LOCK_EX
begin
make_validators res, File.stat(path)
check_validators req, res
rev = File.readlink(path).match(/;([0-9]+)$/)[1].to_i
rescue Errno::EINVAL
File.link path, "#{path};0"
rescue Errno::ENOENT
res.status = 201
end
rev += 1
File.rename tmpn, "#{path};#{rev}"
rpath = path.gsub /.*\//, ''
File.symlink "#{rpath};#{rev}", tmpn
File.rename tmpn, path
make_validators res, File.stat(path)
end
if rev >= 10
begin
File.unlink "#{path};#{rev - 10}"
rescue Errno::ENOENT
end
end
ensure
begin
File.unlink tmpn
rescue Errno::ENOENT
end
end
def make_validators res, stat
mtime = stat.mtime
res['last-modified'] = mtime.httpdate
res['etag'] = etag = sprintf '%x-%x-%x', stat.ino, stat.size, mtime.to_i
end
def check_validators req, res
mtime = Time.parse(res['last-modified'])
etag = res['etag']
if x = req['if-match'] and etag.nil? ||
x != '*' && !WEBrick::HTTPUtils::split_header_value(x).member?(etag)
raise WEBrick::HTTPStatus::PreconditionFailed
end
if x = req['if-none-match'] and
x == '*' || WEBrick::HTTPUtils::split_header_value(x).member?(etag)
if %w(GET HEAD).member?(req.request_method)
raise WEBrick::HTTPStatus::NotModified
else
raise WEBrick::HTTPStatus::PreconditionFailed
end
end
begin
if x = req['if-modified-since'] and
Time.parse(x) >= mtime
raise WEBrick::HTTPStatus::NotModified
end
if x = req['if-unmodified-since'] and
!req['if-match'] && Time.parse(x) < mtime
raise WEBrick::HTTPStatus::PreconditionFailed
end
rescue ArgumentError
# ignore malformed dates
end
end
end
if $0 == __FILE__
server = WEBrick::HTTPServer.new :Port => ENV['PORT'] || 8080
server.mount '/', Tw5
trap 'INT' do
server.shutdown
end
server.start
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment