1
0
Fork 0
mirror of https://github.com/rest-client/rest-client.git synced 2022-11-09 13:49:40 -05:00

Merge commit 'adamwiggins/multipart_streaming' into master

Conflicts:
	README.rdoc
	Rakefile
	lib/request_errors.rb
	lib/resource.rb
	lib/rest_client.rb
	lib/rest_client/request_errors.rb
	lib/rest_client/resource.rb
	lib/restclient/exceptions.rb
	lib/restclient/resource.rb
	rest-client.gemspec
	spec/base.rb
	spec/rest_client_spec.rb
This commit is contained in:
François Beausoleil 2009-08-12 11:00:44 -04:00
commit d3ab28688e
8 changed files with 311 additions and 81 deletions

View file

@ -13,15 +13,38 @@ of specifying actions: get, put, post, delete.
RestClient.post 'http://example.com/resource', :param1 => 'one', :nested => { :param2 => 'two' }
RestClient.delete 'http://example.com/resource'
== Multipart
See RestClient module docs for details.
Yeah, that's right! This does multipart sends for you!
RestClient.post '/data', :myfile => File.new("/path/to/image.jpg")
This does two things for you:
* Auto-detects that you have a File value sends it as multipart
* Auto-detects the mime of the file and sets it in the HEAD of the payload for each entry
If you are sending params that do not contain a File object but the payload needs to be multipart then:
RestClient.post '/data', :foo => 'bar', :multipart => true
== Streaming downloads
RestClient.get('http://some/resource/lotsofdata') do |res|
res.read_body do |chunk|
.. do something with chunk ..
end
end
See RestClient module docs for more details.
== Usage: ActiveResource-Style
resource = RestClient::Resource.new 'http://example.com/resource'
resource.get
private_resource = RestClient::Resource.new 'https://example.com/private/resource', :user => 'adam', :password => 'secret', :timeout => 20, :open_timeout => 5
private_resource = RestClient::Resource.new 'https://example.com/private/resource', 'user', 'pass'
private_resource.put File.read('pic.jpg'), :content_type => 'image/jpg'
See RestClient::Resource module docs for details.
@ -65,83 +88,13 @@ Then invoke:
$ restclient private_site
Use as a one-off, curl-style:
$ restclient get http://example.com/resource > output_body
$ restclient put http://example.com/resource < input_body
== Logging
Write calls to a log filename (can also be "stdout" or "stderr"):
RestClient.log = '/tmp/restclient.log'
Or set an environment variable to avoid modifying the code:
$ RESTCLIENT_LOG=stdout path/to/my/program
Either produces logs like this:
RestClient.get "http://some/resource"
# => 200 OK | text/html 250 bytes
RestClient.put "http://some/resource", "payload"
# => 401 Unauthorized | application/xml 340 bytes
Note that these logs are valid Ruby, so you can paste them into the restclient
shell or a script to replay your sequence of rest calls.
== Proxy
All calls to RestClient, including Resources, will use the proxy specified by
RestClient.proxy:
RestClient.proxy = "http://proxy.example.com/"
RestClient.get "http://some/resource"
# => response from some/resource as proxied through proxy.example.com
Often the proxy url is set in an environment variable, so you can do this to
use whatever proxy the system is configured to use:
RestClient.proxy = ENV['http_proxy']
== Cookies
Request and Response objects know about HTTP cookies, and will automatically
extract and set headers for them as needed:
response = RestClient.get 'http://example.com/action_which_sets_session_id'
response.cookies
# => {"_applicatioN_session_id" => "1234"}
response2 = RestClient.post(
'http://localhost:3000/',
{:param1 => "foo"},
{:cookies => {:session_id => "1234"}}
)
# ...response body
== SSL Client Certificates
RestClient::Resource.new(
'https://example.com',
:ssl_client_cert => OpenSSL::X509::Certificate.new(File.read("cert.pem")),
:ssl_client_key => OpenSSL::PKey::RSA.new(File.read("key.pem"), "passphrase, if any"),
:ssl_ca_file => "ca_certificate.pem",
:verify_ssl => OpenSSL::SSL::VERIFY_PEER
).get
Self-signed certificates can be generated with the openssl command-line tool.
== Meta
Written by Adam Wiggins (adam at heroku dot com)
Patches contributed by: Chris Anderson, Greg Borenstein, Ardekantur, Pedro
Belo, Rafael Souza, Rick Olson, Aman Gupta, Blake Mizerany, Brian Donovan, Ivan
Makfinsky, Marc-André Cournoyer, Coda Hale, Tetsuo Watanabe, Dusty Doris,
Lennon Day-Reynolds, James Edward Gray II, Cyril Rohr, Juan Alvarez, and Adam
Jacob, Paul Dlug, and Brad Ediger
Major modifications by Blake Mizerany
Patches contributed by: Chris Anderson, Greg Borenstein, Ardekantur, Pedro Belo, Rafael Souza, Rick Olson, and Aman Gupta
Released under the MIT License: http://www.opensource.org/licenses/mit-license.php

View file

@ -52,7 +52,6 @@ Rake::RDocTask.new do |t|
t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
t.options << '--charset' << 'utf-8'
t.rdoc_files.include('README.rdoc')
t.rdoc_files.include('lib/restclient.rb')
t.rdoc_files.include('lib/restclient/*.rb')
t.rdoc_files.include('lib/*.rb')
end

View file

@ -15,6 +15,8 @@ require File.dirname(__FILE__) + '/restclient/response'
require File.dirname(__FILE__) + '/restclient/raw_response'
require File.dirname(__FILE__) + '/restclient/resource'
require File.dirname(__FILE__) + '/restclient/exceptions'
require File.dirname(__FILE__) + '/restclient/payload'
require File.dirname(__FILE__) + '/restclient/net_http_ext'
# This module's static methods are the entry point for using the REST client.
#

View file

@ -0,0 +1,21 @@
#
# Replace the request method in Net::HTTP to sniff the body type
# and set the stream if appropriate
#
# Taken from:
# http://www.missiondata.com/blog/ruby/29/streaming-data-to-s3-with-ruby/
module Net
class HTTP
alias __request__ request
def request(req, body=nil, &block)
if body != nil && body.respond_to?(:read)
req.body_stream = body
return __request__(req, nil, &block)
else
return __request__(req, body, &block)
end
end
end
end

184
lib/restclient/payload.rb Normal file
View file

@ -0,0 +1,184 @@
require "tempfile"
require "stringio"
module RestClient
module Payload
extend self
def generate(params)
if params.is_a?(String)
Base.new(params)
elsif params.delete(:multipart) == true ||
params.any? { |_,v| v.respond_to?(:path) && v.respond_to?(:read) }
Multipart.new(params)
else
UrlEncoded.new(params)
end
end
class Base
def initialize(params)
build_stream(params)
end
def build_stream(params)
@stream = StringIO.new(params)
@stream.seek(0)
end
def read(bytes=nil)
@stream.read(bytes)
end
alias :to_s :read
def escape(v)
URI.escape(v.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
end
def headers
{ 'Content-Length' => size.to_s }
end
def size
@stream.size
end
alias :length :size
def close
@stream.close
end
end
class UrlEncoded < Base
def build_stream(params)
@stream = StringIO.new(params.map do |k,v|
"#{escape(k)}=#{escape(v)}"
end.join("&"))
@stream.seek(0)
end
def headers
super.merge({ 'Content-Type' => 'application/x-www-form-urlencoded' })
end
end
class Multipart < Base
EOL = "\r\n"
def build_stream(params)
b = "--#{boundary}"
@stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}")
@stream.write(b + EOL)
params.each do |k,v|
if v.respond_to?(:read) && v.respond_to?(:path)
create_file_field(@stream, k,v)
else
create_regular_field(@stream, k,v)
end
@stream.write(EOL + b)
end
@stream.write('--')
@stream.write(EOL)
@stream.seek(0)
end
def create_regular_field(s, k, v)
s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"")
s.write(EOL)
s.write(EOL)
s.write(v)
end
def create_file_field(s, k, v)
begin
s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"; filename=\"#{v.path}\"#{EOL}")
s.write("Content-Type: #{mime_for(v.path)}#{EOL}")
s.write(EOL)
while data = v.read(8124)
s.write(data)
end
ensure
v.close
end
end
def mime_for(path)
ext = File.extname(path)[1..-1]
MIME_TYPES[ext] || 'text/plain'
end
def boundary
@boundary ||= rand(1_000_000).to_s
end
def headers
super.merge({'Content-Type' => %Q{multipart/form-data; boundary="#{boundary}"}})
end
def close
@stream.close
end
end
# :stopdoc:
# From WEBrick.
MIME_TYPES = {
"ai" => "application/postscript",
"asc" => "text/plain",
"avi" => "video/x-msvideo",
"bin" => "application/octet-stream",
"bmp" => "image/bmp",
"class" => "application/octet-stream",
"cer" => "application/pkix-cert",
"crl" => "application/pkix-crl",
"crt" => "application/x-x509-ca-cert",
"css" => "text/css",
"dms" => "application/octet-stream",
"doc" => "application/msword",
"dvi" => "application/x-dvi",
"eps" => "application/postscript",
"etx" => "text/x-setext",
"exe" => "application/octet-stream",
"gif" => "image/gif",
"gz" => "application/x-gzip",
"htm" => "text/html",
"html" => "text/html",
"jpe" => "image/jpeg",
"jpeg" => "image/jpeg",
"jpg" => "image/jpeg",
"js" => "text/javascript",
"lha" => "application/octet-stream",
"lzh" => "application/octet-stream",
"mov" => "video/quicktime",
"mpe" => "video/mpeg",
"mpeg" => "video/mpeg",
"mpg" => "video/mpeg",
"pbm" => "image/x-portable-bitmap",
"pdf" => "application/pdf",
"pgm" => "image/x-portable-graymap",
"png" => "image/png",
"pnm" => "image/x-portable-anymap",
"ppm" => "image/x-portable-pixmap",
"ppt" => "application/vnd.ms-powerpoint",
"ps" => "application/postscript",
"qt" => "video/quicktime",
"ras" => "image/x-cmu-raster",
"rb" => "text/plain",
"rd" => "text/plain",
"rtf" => "application/rtf",
"sgm" => "text/sgml",
"sgml" => "text/sgml",
"tif" => "image/tiff",
"tiff" => "image/tiff",
"txt" => "text/plain",
"xbm" => "image/x-xbitmap",
"xls" => "application/vnd.ms-excel",
"xml" => "text/xml",
"xpm" => "image/x-xpixmap",
"xwd" => "image/x-xwindowdump",
"zip" => "application/zip",
}
# :startdoc:
end
end

View file

@ -6,7 +6,7 @@ Gem::Specification.new do |s|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Adam Wiggins"]
s.date = %q{2009-07-29}
s.date = %q{2009-08-12}
s.default_executable = %q{restclient}
s.description = %q{A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete.}
s.email = %q{adam@heroku.com}
@ -20,6 +20,8 @@ Gem::Specification.new do |s|
"VERSION",
"bin/restclient",
"lib/rest_client.rb",
"lib/rest_client/net_http_ext.rb",
"lib/rest_client/payload.rb",
"lib/restclient.rb",
"lib/restclient/exceptions.rb",
"lib/restclient/mixin/response.rb",
@ -29,24 +31,26 @@ Gem::Specification.new do |s|
"lib/restclient/response.rb",
"spec/base.rb",
"spec/exceptions_spec.rb",
"spec/master_shake.jpg",
"spec/mixin/response_spec.rb",
"spec/payload_spec.rb",
"spec/raw_response_spec.rb",
"spec/request_spec.rb",
"spec/resource_spec.rb",
"spec/response_spec.rb",
"spec/restclient_spec.rb"
]
s.has_rdoc = true
s.homepage = %q{http://rest-client.heroku.com/}
s.rdoc_options = ["--charset=UTF-8"]
s.require_paths = ["lib"]
s.rubyforge_project = %q{rest-client}
s.rubygems_version = %q{1.3.1}
s.rubygems_version = %q{1.3.5}
s.summary = %q{Simple REST client for Ruby, inspired by microframework syntax for specifying actions.}
s.test_files = [
"spec/base.rb",
"spec/exceptions_spec.rb",
"spec/mixin/response_spec.rb",
"spec/payload_spec.rb",
"spec/raw_response_spec.rb",
"spec/request_spec.rb",
"spec/resource_spec.rb",
@ -56,7 +60,7 @@ Gem::Specification.new do |s|
if s.respond_to? :specification_version then
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
s.specification_version = 2
s.specification_version = 3
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
else

BIN
spec/master_shake.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

67
spec/payload_spec.rb Normal file
View file

@ -0,0 +1,67 @@
require File.dirname(__FILE__) + "/base"
describe RestClient::Payload do
context "A regular Payload" do
it "should should default content-type to standard enctype" do
RestClient::Payload::UrlEncoded.new({}).headers['Content-Type'].
should == 'application/x-www-form-urlencoded'
end
it "should form properly encoded params" do
RestClient::Payload::UrlEncoded.new({:foo => 'bar'}).to_s.
should == "foo=bar"
end
end
context "A multipart Payload" do
it "should should default content-type to standard enctype" do
m = RestClient::Payload::Multipart.new({})
m.stub!(:boundary).and_return(123)
m.headers['Content-Type'].should == 'multipart/form-data; boundary="123"'
end
it "should form properly seperated multipart data" do
m = RestClient::Payload::Multipart.new({:foo => "bar"})
m.to_s.should == <<-EOS
--#{m.boundary}\r
Content-Disposition: multipart/form-data; name="foo"\r
\r
bar\r
--#{m.boundary}--\r
EOS
end
it "should form properly seperated multipart data" do
f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
m = RestClient::Payload::Multipart.new({:foo => f})
m.to_s.should == <<-EOS
--#{m.boundary}\r
Content-Disposition: multipart/form-data; name="foo"; filename="./spec/master_shake.jpg"\r
Content-Type: image/jpeg\r
\r
#{IO.read(f.path)}\r
--#{m.boundary}--\r
EOS
end
end
context "Payload generation" do
it "should recognize standard urlencoded params" do
RestClient::Payload.generate({"foo" => 'bar'}).should be_kind_of(RestClient::Payload::UrlEncoded)
end
it "should recognize multipart params" do
f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
RestClient::Payload.generate({"foo" => f}).should be_kind_of(RestClient::Payload::Multipart)
end
it "should be multipart if forced" do
RestClient::Payload.generate({"foo" => "bar", :multipart => true}).should be_kind_of(RestClient::Payload::Multipart)
end
it "should return data if no of the above" do
RestClient::Payload.generate("data").should be_kind_of(RestClient::Payload::Base)
end
end
end