Skip to content

Commit

Permalink
Add form_multipart for file uploads
Browse files Browse the repository at this point in the history
  • Loading branch information
andyjeffries committed Sep 18, 2019
1 parent 5845016 commit d4dc24f
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 1 deletion.
6 changes: 6 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog

## 1.8.2

Features:

- Allowed use of `:form_multipart` as a `request_body_type` to allow uploading files to an API.

## 1.8.1

Features:
Expand Down
2 changes: 2 additions & 0 deletions docs/body-types.md
Expand Up @@ -17,6 +17,8 @@ Flexirest::Base.request_body_type = :json

This will also set the header `Content-Type` to `application/x-www-form-urlencoded` by default or `application/json; charset=utf-8` when `:json`. You can override this using the callback `before_request`.

You can also use `:form_multipart` if your API requires file uploads. Any parameters set to `File` like object (supporting `#path` and `#read`) will be automatically uploaded with the parameters, in a normal form submission style.

If you have an API that is inconsistent in its body type requirements, you can also specify it on the individual method mapping:

```ruby
Expand Down
1 change: 1 addition & 0 deletions lib/flexirest.rb
Expand Up @@ -13,6 +13,7 @@
require "flexirest/headers_list"
require "flexirest/lazy_loader"
require "flexirest/lazy_association_loader"
require "flexirest/multipart"
require "flexirest/json_api_proxy"
require "flexirest/request"
require "flexirest/request_delegator"
Expand Down
81 changes: 81 additions & 0 deletions lib/flexirest/multipart.rb
@@ -0,0 +1,81 @@
# Takes a hash of string and file parameters and returns a string of text
# formatted to be sent as a multipart form post.
#
# Author:: Cody Brimhall <mailto:brimhall@somuchwit.com>
# Created:: 22 Feb 2008
# License:: Distributed under the terms of the WTFPL (http://www.wtfpl.net/txt/copying/)

require 'rubygems'
require 'mime/types'
require 'cgi'


module Flexirest
module Multipart
VERSION = "1.0.0"

# Formats a given hash as a multipart form post
# If a hash value responds to :string or :read messages, then it is
# interpreted as a file and processed accordingly; otherwise, it is assumed
# to be a string
class Post
BOUNDARY = "FLEXIRESTBOUNDARY-20190918-FLEXIRESTBOUNDARY"
CONTENT_TYPE = "multipart/form-data; boundary=#{BOUNDARY}"
HEADER = {"Content-Type" => CONTENT_TYPE}

def self.prepare_query(params)
fp = []

params.stringify_keys.each do |k, v|
# Are we trying to make a file parameter?
if v.respond_to?(:path) and v.respond_to?(:read) then
fp.push(FileParam.new(k, v.path, v.read))
# We must be trying to make a regular parameter
else
fp.push(StringParam.new(k, v))
end
end

# Assemble the request body using the special multipart format
query = fp.collect {|p| "--" + BOUNDARY + "\r\n" + p.to_multipart }.join("") + "--" + BOUNDARY + "--"
return query, HEADER
end
end

private

# Formats a basic string key/value pair for inclusion with a multipart post
class StringParam
attr_accessor :k, :v

def initialize(k, v)
@k = k
@v = v
end

def to_multipart
return "Content-Disposition: form-data; name=\"#{CGI::escape(k)}\"\r\n\r\n#{v}\r\n"
end
end

# Formats the contents of a file or string for inclusion with a multipart
# form post
class FileParam
attr_accessor :k, :filename, :content

def initialize(k, filename, content)
@k = k
@filename = filename
@content = content
end

def to_multipart
# If we can tell the possible mime-type from the filename, use the
# first in the list; otherwise, use "application/octet-stream"
mime_type = MIME::Types.type_for(filename)[0] || MIME::Types["application/octet-stream"][0]
return "Content-Disposition: form-data; name=\"#{CGI::escape(k)}\"; filename=\"#{ filename }\"\r\n" +
"Content-Type: #{ mime_type.simplified }\r\n\r\n#{ content }\r\n"
end
end
end
end
19 changes: 19 additions & 0 deletions lib/flexirest/request.rb
Expand Up @@ -400,6 +400,8 @@ def prepare_request_body(params = nil)
elsif http_method == :get || (http_method == :delete && !@method[:options][:send_delete_body])
if request_body_type == :form_encoded
headers["Content-Type"] ||= "application/x-www-form-urlencoded; charset=utf-8"
elsif request_body_type == :form_multipart
headers["Content-Type"] ||= "multipart/form-data; charset=utf-8"
elsif request_body_type == :json
headers["Content-Type"] ||= "application/json; charset=utf-8"
end
Expand All @@ -417,6 +419,23 @@ def prepare_request_body(params = nil)
p.to_query
end
headers["Content-Type"] ||= "application/x-www-form-urlencoded"
elsif request_body_type == :form_multipart
headers["Content-Type"] ||= "multipart/form-data; charset=utf-8"
@body ||= if params.is_a?(String)
params
elsif @post_params.is_a?(String)
@post_params
else
p = (params || @post_params || {})
if @method[:options][:wrap_root].present?
p = {@method[:options][:wrap_root] => p}
end
data, mp_headers = Flexirest::Multipart::Post.prepare_query(p)
mp_headers.each do |k,v|
headers[k] = v
end
data
end
elsif request_body_type == :json
@body ||= if params.is_a?(String)
params
Expand Down
2 changes: 1 addition & 1 deletion lib/flexirest/version.rb
@@ -1,3 +1,3 @@
module Flexirest
VERSION = "1.8.1"
VERSION = "1.8.2"
end
22 changes: 22 additions & 0 deletions spec/lib/request_spec.rb
Expand Up @@ -353,6 +353,28 @@ class WhitelistedDateClient < Flexirest::Base
ExampleClient.update id:1234, debug:true, test:'foo'
end

it "should encode the body in a form-encoded format by default" do
body = "--FLEXIRESTBOUNDARY-20190918-FLEXIRESTBOUNDARY\r\n" +
"Content-Disposition: form-data; name=\"debug\"\r\n" +
"\r\n" +
"true\r\n" +
"--FLEXIRESTBOUNDARY-20190918-FLEXIRESTBOUNDARY\r\n" +
"Content-Disposition: form-data; name=\"test\"\r\n" +
"\r\n" +
"foo\r\n" +
"--FLEXIRESTBOUNDARY-20190918-FLEXIRESTBOUNDARY\r\n" +
"Content-Disposition: form-data; name=\"file\"; filename=\"#{File.dirname(__FILE__)}/../../spec/samples/file.txt\"\r\n" +
"Content-Type: text/plain\r\n" +
"\r\n" +
"The quick brown fox jumps over the lazy dog\n\r\n" +
"--FLEXIRESTBOUNDARY-20190918-FLEXIRESTBOUNDARY--"
expect_any_instance_of(Flexirest::Connection).to receive(:put).with(
"/put/1234", body, hash_including(headers: hash_including("Content-Type"=>"multipart/form-data; boundary=FLEXIRESTBOUNDARY-20190918-FLEXIRESTBOUNDARY"))
).and_return(::FaradayResponseMock.new(OpenStruct.new(body:"{\"result\":true}", response_headers:{})))
ExampleClient.request_body_type :form_multipart
ExampleClient.update id:1234, debug:true, test:'foo', file: File.open("#{File.dirname(__FILE__)}/../../spec/samples/file.txt")
end

it "should encode the body in a JSON format if specified" do
expect_any_instance_of(Flexirest::Connection).to receive(:put).with("/put/1234", %q({"debug":true,"test":"foo"}), an_instance_of(Hash)).and_return(::FaradayResponseMock.new(OpenStruct.new(body:"{\"result\":true}", response_headers:{})))
ExampleClient.request_body_type :json
Expand Down
1 change: 1 addition & 0 deletions spec/samples/file.txt
@@ -0,0 +1 @@
The quick brown fox jumps over the lazy dog

0 comments on commit d4dc24f

Please sign in to comment.