From 909d75e98a2d27b6d831124effbe8f73ee4ce498 Mon Sep 17 00:00:00 2001 From: Paul Duncan Date: Sun, 13 Mar 2016 17:09:40 -0400 Subject: add guff-stuff static files handler --- data/stuff/test.js | 4 +++ src/guff/config.cr | 54 +++++++++++++++++++++++------ src/guff/handlers.cr | 5 +++ src/guff/handlers/guff-stuff.cr | 77 +++++++++++++++++++++++++++++++++++++++++ src/guff/mime-type.cr | 14 ++++++++ 5 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 data/stuff/test.js create mode 100644 src/guff/handlers/guff-stuff.cr create mode 100644 src/guff/mime-type.cr diff --git a/data/stuff/test.js b/data/stuff/test.js new file mode 100644 index 0000000..461b854 --- /dev/null +++ b/data/stuff/test.js @@ -0,0 +1,4 @@ + +function test() { + return true; +} diff --git a/src/guff/config.cr b/src/guff/config.cr index 1be8d8d..43eaf13 100644 --- a/src/guff/config.cr +++ b/src/guff/config.cr @@ -1,4 +1,5 @@ require "option_parser" +require "yaml" module Guff class Config @@ -8,6 +9,7 @@ module Guff "data": "./data", "public": "./public", "environment": "development", + "stuff": "/usr/local/share/guff/stuff", } DIRS = %w{ @@ -38,10 +40,21 @@ module Guff private def parse_args(app, args) OptionParser.parse(args) do |p| + p.on( + "--config PATH", + "Read config from YAML file." + ) do |path| + YAML.parse(File.read(path)).each do |k, v| + self[k.as_s] = v.as_s + end + end + p.on( "-h HOST", "--host HOST", - "Host (defaults to \"localhost\")." + "Host (defaults to \"%s\")." % [ + DEFAULTS["host"] + ] ) do |host| self["host"] = host end @@ -49,7 +62,9 @@ module Guff p.on( "-p PORT", "--port PORT", - "Port (defaults to 8989)." + "Port (defaults to %s)." % [ + DEFAULTS["port"] + ] ) do |port| self["port"] = port end @@ -57,23 +72,37 @@ module Guff p.on( "-d PATH", "--data-dir PATH", - "Path to data directory (defaults to \"./data\")." + "Path to data directory (defaults to \"%s\")." % [ + DEFAULTS["data"], + ] ) do |path| self["data"] = path end - + p.on( - "-u PATH", "--public-dir PATH", - "Path to public directory (defaults to \"./public\")." + "Path to public directory (defaults to \"%s\")." % [ + DEFAULTS["public"], + ] ) do |path| self["public"] = path end + p.on( + "--stuff-dir PATH", + "Path to Guff static files directory (defaults to \"%s\")." % [ + DEFAULTS["stuff"], + ] + ) do |path| + self["stuff"] = path + end + p.on( "-e ENV", "--environment ENV", - "Environment (defaults to \"development\")." + "Environment (defaults to \"%s\")." % [ + DEFAULTS["environment"], + ] ) do |path| self["public"] = path end @@ -82,7 +111,7 @@ module Guff p.on( "--help", "Print usage" - ) do + ) do puts p.to_s exit 0 end @@ -90,9 +119,14 @@ module Guff # expand output directory paths and create them %w{data public}.each do |key| - self[key] = File.expand_path(File.join(File.dirname(app), self[key])) + # expand path (unless it is absolute) + self[key] = File.expand_path(File.join( + File.dirname(app), + self[key] + )) unless self[key][0] == '/' + # puts "DEBUG: %s: %s" % [key, self[key]] - Dir.mkdir(self[key], 0o770) unless File.directory?(self[key]) + Dir.mkdir(self[key], 0o770) unless Dir.exists?(self[key]) end end end diff --git a/src/guff/handlers.cr b/src/guff/handlers.cr index 834be47..91bc42b 100644 --- a/src/guff/handlers.cr +++ b/src/guff/handlers.cr @@ -19,6 +19,11 @@ module Guff init: ->(models : Models) { HTTP::DeflateHandler.new as HTTP::Handler }, + }, { + env: %w{development}, + init: ->(models : Models) { + GuffStuffHandler.new(models) as HTTP::Handler + }, }, { env: %w{development}, init: ->(models : Models) { diff --git a/src/guff/handlers/guff-stuff.cr b/src/guff/handlers/guff-stuff.cr new file mode 100644 index 0000000..cc3ac6c --- /dev/null +++ b/src/guff/handlers/guff-stuff.cr @@ -0,0 +1,77 @@ +require "uri" +require "../handler" +require "../mime-type" +require "../views/html/page" + +class Guff::Handlers::GuffStuffHandler < Guff::Handler + def initialize(models) + super(models) + @digests = {} of String => String + end + + def call(context : HTTP::Server::Context) + path = context.request.path.not_nil! + + if matching_request?(context.request.method, path) + reply(context) + else + call_next(context) + end + end + + private def reply(context : HTTP::Server::Context) + # get expanded path to file + if path = expand_path(context.request.path.not_nil!) + # get file digest + file_digest = digest(path) + + # check for cache header + if context.request.headers["if-none-match"]? == file_digest + # cached, send 304 not modified + context.response.status_code = 304 + else + # not cached, set code and send headers + context.response.status_code = 200 + context.response.content_type = Guff::MimeType.mime_type(path) + context.response.content_length = File.size(path) + context.response.headers["etag"] = file_digest + + if context.request.method == "GET" + # send body + File.open(path) do |fh| + IO.copy(fh, context.response) + end + end + end + else + # file not found + context.response.status_code = 404 + end + end + + VALID_METHODS = %w{GET HEAD} + PATH_RE = %r{^/guff-stuff/} + + private def matching_request?(method, path) + VALID_METHODS.includes?(method) && PATH_RE.match(path) + end + + private def expand_path(req_path : String) : String? + # unescape path, check for nil byte + path = URI.unescape(req_path) + return nil if path.includes?('\0') + + # build absolute path + r = File.join( + @models.config["stuff"], + File.expand_path(path.gsub(PATH_RE, ""), "/") + ) + + # return path if file exists, or nil otherwise + File.file?(r) ? r : nil + end + + private def digest(path : String) : String + @digests[path] ||= OpenSSL::Digest.new("SHA1").file(path).hexdigest + end +end diff --git a/src/guff/mime-type.cr b/src/guff/mime-type.cr new file mode 100644 index 0000000..992521b --- /dev/null +++ b/src/guff/mime-type.cr @@ -0,0 +1,14 @@ +module Guff::MimeType + TYPES = { + ".js": "text/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".png": "image/png", + ".jpeg": "image/jpeg", + ".jpg": "image/jpeg", + } + + def self.mime_type(path : String) : String + TYPES[File.extname(path)]? || "application/octet-stream" + end +end -- cgit v1.2.3