Skip to content

Commit

Permalink
Better file upload
Browse files Browse the repository at this point in the history
  • Loading branch information
Serdar Dogruyol committed Feb 11, 2017
1 parent 72dc6cf commit 6fe57d5
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 32 deletions.
12 changes: 12 additions & 0 deletions src/kemal/file_upload.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# :nodoc:
struct FileUpload
getter tmpfile : Tempfile
getter tmpfile_path : String
getter filename : String
getter meta : HTTP::FormData::FileMetadata
getter headers : HTTP::Headers

def initialize(@tmpfile, @tmpfile_path, @meta, @headers)
@filename = @meta.filename.not_nil!
end
end
30 changes: 0 additions & 30 deletions src/kemal/helpers/helpers.cr
Original file line number Diff line number Diff line change
Expand Up @@ -72,33 +72,3 @@ end
def gzip(status : Bool = false)
add_handler HTTP::DeflateHandler.new if status
end

# :nodoc:
struct UploadFile
getter field : String
getter data : IO::Delimited
getter meta : HTTP::FormData::FileMetadata
getter headers : HTTP::Headers

def initialize(@field, @data, @meta, @headers)
end
end

# Parses a multipart/form-data request. Yields an `UploadFile` object with `field`, `data`, `meta`, `headers` fields.
# Consider the example below taking two image uploads as image1, image2. To get the relevant data
# for each file you can use simple `if/switch` conditionals.
#
# post "/upload" do |env|
# parse_multipart(env) do |f|
# image1 = f.data if f.field == "image1"
# image2 = f.data if f.field == "image2"
# puts f.meta
# puts f.headers
# "Upload complete"
# end
# end
def parse_multipart(env)
HTTP::FormData.parse(env.request) do |field, data, meta, headers|
yield UploadFile.new field, data, meta, headers
end
end
32 changes: 30 additions & 2 deletions src/kemal/param_parser.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "json"
require "uri"
require "tempfile"

module Kemal
# ParamParser parses the request contents including query_params and body
Expand All @@ -8,14 +9,17 @@ module Kemal
class ParamParser
URL_ENCODED_FORM = "application/x-www-form-urlencoded"
APPLICATION_JSON = "application/json"
MULTIPART_FORM = "multipart/form-data"
# :nodoc:
alias AllParamTypes = Nil | String | Int64 | Float64 | Bool | Hash(String, JSON::Type) | Array(JSON::Type)
getter files

def initialize(@request : HTTP::Request)
@url = {} of String => String
@query = HTTP::Params.new({} of String => Array(String))
@body = HTTP::Params.new({} of String => Array(String))
@json = {} of String => AllParamTypes
@files = {} of String => FileUpload
@url_parsed = false
@query_parsed = false
@body_parsed = false
Expand All @@ -41,8 +45,16 @@ module Kemal
{% end %}

def parse_body
return if (@request.headers["Content-Type"]? =~ /#{URL_ENCODED_FORM}/).nil?
@body = parse_part(@request.body)
content_type = @request.headers["Content-Type"]?
return unless content_type
if content_type.try(&.starts_with?(URL_ENCODED_FORM))
@body = parse_part(@request.body)
return
end
if content_type.try(&.starts_with?(MULTIPART_FORM))
parse_file_upload
return
end
end

def parse_query
Expand All @@ -57,6 +69,22 @@ module Kemal
end
end

def parse_file_upload
HTTP::FormData.parse(@request) do |field, data, meta, headers|
next unless meta
filename = meta.filename
if !filename.nil?
tempfile = Tempfile.new(filename)
::File.open(tempfile.path, "w") do |file|
IO.copy(data, file)
end
@files[field] = FileUpload.new(tmpfile: tempfile, tmpfile_path: tempfile.path, meta: meta, headers: headers)
else
@body[field] = data.gets_to_end
end
end
end

# Parses JSON request body if Content-Type is `application/json`.
# If request body is a JSON Hash then all the params are parsed and added into `params`.
# If request body is a JSON Array it's added into `params` as `_json` and can be accessed
Expand Down
8 changes: 8 additions & 0 deletions src/kemal/route_handler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,21 @@ module Kemal
raise Kemal::Exceptions::RouteNotFound.new(context) unless context.route_defined?
route = context.route_lookup.payload.as(Route)
content = route.handler.call(context)
ensure
remove_tmpfiles(context)
if Kemal.config.error_handlers.has_key?(context.response.status_code)
raise Kemal::Exceptions::CustomException.new(context)
end
context.response.print(content)
context
end

private def remove_tmpfiles(context)
context.params.files.each do |field, file|
File.delete(file.tmpfile_path) if ::File.exists?(file.tmpfile_path)
end
end

private def radix_path(method : String, path)
"/#{method.downcase}#{path}"
end
Expand Down

0 comments on commit 6fe57d5

Please sign in to comment.