about summary refs log tree commit homepage
path: root/extras/try_gzip_static.rb
diff options
context:
space:
mode:
Diffstat (limited to 'extras/try_gzip_static.rb')
-rw-r--r--extras/try_gzip_static.rb208
1 files changed, 208 insertions, 0 deletions
diff --git a/extras/try_gzip_static.rb b/extras/try_gzip_static.rb
new file mode 100644
index 0000000..efe47f9
--- /dev/null
+++ b/extras/try_gzip_static.rb
@@ -0,0 +1,208 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require 'time'
+require 'rack/utils'
+require 'rack/mime'
+require 'kgio'
+
+class TryGzipStatic
+  attr_accessor :root
+  class KF < Kgio::File
+    # attr_writer :sf_range
+
+    # only used if the server does not handle #to_path,
+    # yahns should never hit this
+    def each
+      raise "we should never get here in yahns"
+      buf = ""
+      rsize = 8192
+      if @sf_range
+        file.seek(@sf_range.begin)
+        sf_count = @sf_range.end - @sf_range.begin + 1
+        while sf_count > 0
+          read(sf_count > rsize ? rsize : sf_count, buf) or break
+          sf_count -= buf.size
+          yield buf
+        end
+        raise "file truncated" if sf_count != 0
+      else
+        yield(buf) while read(rsize, buf)
+      end
+    end
+  end
+
+  def initialize(root, default_type = 'text/plain')
+    @root = root
+    @default_type = default_type
+  end
+
+  def fspath(env)
+    path_info = Rack::Utils.unescape(env["PATH_INFO"])
+    path_info =~ /\.\./ ? nil : "#@root#{path_info}"
+  end
+
+  def get_range(env, path, st)
+    if ims = env["HTTP_IF_MODIFIED_SINCE"]
+      return [ 304, {}, [] ] if st.mtime.httpdate == ims
+    end
+
+    size = st.size
+    ranges = Rack::Utils.byte_ranges(env, size)
+    if ranges.nil? || ranges.length > 1
+      [ 200 ] # serve the whole thing, possibly with static gzip \o/
+    elsif ranges.empty?
+      res = r(416)
+      res[1]["Content-Range"] = "bytes */#{size}"
+      res
+    else # partial response, no using static gzip file
+      range = ranges[0]
+      len = range.end - range.begin + 1
+      h = fheader(env, path, st, nil, len)
+      h["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}"
+      [ 206, h, range ]
+    end
+  end
+
+  def fheader(env, path, st, gz_st = nil, len = nil)
+    if path =~ /(.[^.]+)\z/
+      mime = Rack::Mime.mime_type($1, @default_type)
+    else
+      mime = @default_type
+    end
+    len ||= (gz_st ? gz_st : st).size
+    h = {
+      "Content-Type" => mime,
+      "Content-Length" => len.to_s,
+      "Last-Modified" => st.mtime.httpdate,
+      "Accept-Ranges" => "bytes",
+    }
+    h["Content-Encoding"] = "gzip" if gz_st
+    h
+  end
+
+  def head_no_gz(res, env, path, st)
+    res[1] = fheader(env, path, st)
+    res[2] = [] # empty body
+    res
+  end
+
+  def stat_path(env)
+    path = fspath(env) or return r(403)
+    begin
+      st = File.stat(path)
+      st.file? ? [ path, st ] : r(404)
+    rescue Errno::ENOENT
+      r(404)
+    rescue Errno::EACCES
+      r(403)
+    rescue => e
+      r(500, e.message, env)
+    end
+  end
+
+  def head(env)
+    path, st = res = stat_path(env)
+    return res if Integer === path # integer status code on failure
+
+    # see if it's a range request, no gzipped version if so
+    status, _ = res = get_range(env, path, st)
+    case status
+    when 206
+      res[2] = [] # empty body, headers  are all set
+      res
+    when 200 # fall through to trying gzipped version
+      # client requested gzipped path explicitly or did not want gzip
+      if path =~ /\.gz\z/i || !want_gzip?(env)
+        head_no_gz(res, env, path, st)
+      else # try the gzipped version
+        begin
+          gz_st = File.stat("#{path}.gz")
+          if gz_st.mtime == st.mtime
+            res[1] = fheader(env, path, st, gz_st)
+            res[2] = []
+            res
+          else
+            head_no_gz(res, env, path, st)
+          end
+        rescue Errno::ENOENT, Errno::EACCES
+          head_no_gz(res, env, path, st)
+        rescue => e
+          r(500, e.message, env)
+        end
+      end
+    else # 416, 304
+      res
+    end
+  end
+
+  def call(env)
+    case env["REQUEST_METHOD"]
+    when "GET" then get(env)
+    when "HEAD" then head(env)
+    else r(405)
+    end
+  end
+
+  def want_gzip?(env)
+    env["HTTP_ACCEPT_ENCODING"] =~ /\bgzip\b/i
+  end
+
+  def get(env)
+    path, st, _ = res = stat_path(env)
+    return res if Integer === path # integer status code on failure
+
+    # see if it's a range request, no gzipped version if so
+    status, _, _ = res = get_range(env, path, st)
+    case status
+    when 206
+      res[2] = KF.open(path) # stat succeeded
+    when 200
+      # client requested gzipped path explicitly or did not want gzip
+      if path =~ /\.gz\z/i || !want_gzip?(env)
+        res[1] = fheader(env, path, st)
+        res[2] = KF.open(path)
+      else
+        case gzbody = KF.tryopen("#{path}.gz")
+        when KF
+          gz_st = gzbody.stat
+          if gz_st.file? && gz_st.mtime == st.mtime
+            # yay! serve the gzipped version as the regular one
+            # this should be the most likely code path
+            res[1] = fheader(env, path, st, gz_st)
+            res[2] = gzbody
+          else
+            gzbody.close
+            res[1] = fheader(env, path, st)
+            res[2] = KF.open(path)
+          end
+        when :ENOENT, :EACCES
+          res[1] = fheader(env, path, st)
+          res[2] = KF.open(path)
+        else
+          res = r(500, gzbody.to_s, env)
+        end
+      end
+    end
+    res
+  rescue Errno::ENOENT # could get here from a race
+    r(404)
+  rescue Errno::EACCES # could get here from a race
+    r(403)
+  rescue => e
+    r(500, e.message, env)
+  end
+
+  def r(code, msg = nil, env = nil)
+    if env && logger = env["rack.logger"]
+      logger.warn("#{env['REQUEST_METHOD']} #{env['PATH_INFO']} " \
+                  "#{code} #{msg.inspect}")
+    end
+
+    if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(code)
+      [ code, {}, [] ]
+    else
+      h = { 'Content-Type' => 'text/plain', 'Content-Length' => '0' }
+      [ code, h, [] ]
+    end
+  end
+end