Ruby: 如何通过 HTTP 将文件作为多部分/表单数据发布?

我想做一个 HTTPPOST,它看起来像从浏览器发布的 HMTL 表单。特别是,发布一些文本字段和文件字段。

发布文本字段很简单,在 net/http rdocs 中就有一个例子,但是我不知道如何将文件与它一起发布。

HTTP 看起来不是最好的主意。 路边看起来不错。

131098 次浏览

好的,这里有一个使用遏制的简单例子。

require 'yaml'
require 'curb'


# prepare post data
post_data = fields_hash.map { |k, v| Curl::PostField.content(k, v.to_s) }
post_data << Curl::PostField.file('file', '/path/to/file'),


# post
c = Curl::Easy.new('http://localhost:3000/foo')
c.multipart_form_post = true
c.http_post(post_data)


# print response
y [c.response_code, c.body_str]

curb looks like a great solution, but in case it doesn't meet your needs, you 可以 do it with Net::HTTP. A multipart form post is just a carefully-formatted string with some extra headers. It seems like every Ruby programmer who needs to do multipart posts ends up writing their own little library for it, which makes me wonder why this functionality isn't built-in. Maybe it is... Anyway, for your reading pleasure, I'll go ahead and give my solution here. This code is based off of examples I found on a couple of blogs, but I regret that I can't find the links anymore. So I guess I just have to take all the credit for myself...

我为此编写的模块包含一个公共类,用于从 StringFile对象的散列中生成表单数据和头文件。例如,如果希望发布一个包含名为“ title”的字符串参数和名为“ document”的文件参数的表单,可以执行以下操作:

#prepare the query
data, headers = Multipart::Post.prepare_query("title" => my_string, "document" => my_file)

然后你只需要做一个正常的 POSTNet::HTTP:

http = Net::HTTP.new(upload_uri.host, upload_uri.port)
res = http.start {|con| con.post(upload_uri.path, data, headers) }

或者其他你想做的 POST。关键是 Multipart返回需要发送的数据和头。就是这样!很简单,对吧?下面是 Multipart 模块的代码(您需要 mime-types gem) :

# 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 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
# We have to pretend we're a web browser...
USERAGENT = "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6"
BOUNDARY = "0123456789ABLEWASIEREISAWELBA9876543210"
CONTENT_TYPE = "multipart/form-data; boundary=#{ BOUNDARY }"
HEADER = { "Content-Type" => CONTENT_TYPE, "User-Agent" => USERAGENT }


def self.prepare_query(params)
fp = []


params.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

我喜欢 RestClient,它用很酷的功能封装了 net/http,比如多部分表单数据:

require 'rest_client'
RestClient.post('http://localhost:3000/foo',
:name_of_file_param => File.new('/path/to/file'))

它还支持流媒体。

gem install rest-client会让你开始。

NetHttp 的解决方案有一个缺点,就是当发布大文件时,它会首先将整个文件加载到内存中。

After playing a bit with it I came up with the following solution:

class Multipart


def initialize( file_names )
@file_names = file_names
end


def post( to_url )
boundary = '----RubyMultipartClient' + rand(1000000).to_s + 'ZZZZZ'


parts = []
streams = []
@file_names.each do |param_name, filepath|
pos = filepath.rindex('/')
filename = filepath[pos + 1, filepath.length - pos]
parts << StringPart.new ( "--" + boundary + "\r\n" +
"Content-Disposition: form-data; name=\"" + param_name.to_s + "\"; filename=\"" + filename + "\"\r\n" +
"Content-Type: video/x-msvideo\r\n\r\n")
stream = File.open(filepath, "rb")
streams << stream
parts << StreamPart.new (stream, File.size(filepath))
end
parts << StringPart.new ( "\r\n--" + boundary + "--\r\n" )


post_stream = MultipartStream.new( parts )


url = URI.parse( to_url )
req = Net::HTTP::Post.new(url.path)
req.content_length = post_stream.size
req.content_type = 'multipart/form-data; boundary=' + boundary
req.body_stream = post_stream
res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) }


streams.each do |stream|
stream.close();
end


res
end


end


class StreamPart
def initialize( stream, size )
@stream, @size = stream, size
end


def size
@size
end


def read ( offset, how_much )
@stream.read ( how_much )
end
end


class StringPart
def initialize ( str )
@str = str
end


def size
@str.length
end


def read ( offset, how_much )
@str[offset, how_much]
end
end


class MultipartStream
def initialize( parts )
@parts = parts
@part_no = 0;
@part_offset = 0;
end


def size
total = 0
@parts.each do |part|
total += part.size
end
total
end


def read ( how_much )


if @part_no >= @parts.size
return nil;
end


how_much_current_part = @parts[@part_no].size - @part_offset


how_much_current_part = if how_much_current_part > how_much
how_much
else
how_much_current_part
end


how_much_next_part = how_much - how_much_current_part


current_part = @parts[@part_no].read(@part_offset, how_much_current_part )


if how_much_next_part > 0
@part_no += 1
@part_offset = 0
next_part = read ( how_much_next_part  )
current_part + if next_part
next_part
else
''
end
else
@part_offset += how_much_current_part
current_part
end
end
end

这里是我的解决方案,在尝试了其他可用的在这篇文章,我用它来上传图片在 TwitPic:

  def upload(photo)
`curl -F media=@#{photo.path} -F username=#{@username} -F password=#{@password} -F message='#{photo.title}' http://twitpic.com/api/uploadAndPost`
end

我也有同样的问题(需要发布到 jboss web 服务器)。Curb 对我来说很好用,除了当我在代码中使用会话变量时,它导致 Ruby 崩溃(ubuntu 8.10上的 ruby 1.8.7)。

我深入研究了剩余的客户端文档,没有发现多部分支持的迹象。我尝试了上面的 rest-client 示例,但是 jboss 说 http post 不是多部分的。

还有 Nick Sieger 的 多部分岗位多部分岗位可供选择。

在我覆盖 RestClient: : Payload: : Multipart 中的 create _ file _ field 之前,RestClient 不能为我工作。

它在每个应该是 「内容-处置: 表格-资料」的地方都创建了一个 'Content-Disposition: multipart/form-data'

Http://www.ietf.org/rfc/rfc2388.txt

如果你需要,我的叉子在这里: git@github.com: kCrawford/rest-client. git

关于尼克 · 西格尔的多部分文章图书馆,我怎么说都不够。

它添加了对多部分直接发布到 Net: : HTTP 的支持,消除了您需要手动担心的边界或大型库,这些边界或库可能与您自己的目标不同。

下面是一个关于如何使用 自述的小例子:

require 'net/http/post/multipart'


url = URI.parse('http://www.example.com/upload')
File.open("./image.jpg") do |jpg|
req = Net::HTTP::Post::Multipart.new url.path,
"file" => UploadIO.new(jpg, "image/jpeg", "image.jpg")
res = Net::HTTP.start(url.host, url.port) do |http|
http.request(req)
end
end

你可以在这里查阅图书馆: Http://github.com/nicksieger/multipart-post

或安装:

$ sudo gem install multipart-post

如果你通过 SSL 连接,你需要这样开始连接:

n = Net::HTTP.new(url.host, url.port)
n.use_ssl = true
# for debugging dev server
#n.verify_mode = OpenSSL::SSL::VERIFY_NONE
res = n.start do |http|

Multipart-post gem 与 Rails 4 Net: : HTTP 工作得非常好,没有其他特殊 gem

def model_params
require_params = params.require(:model).permit(:param_one, :param_two, :param_three, :avatar)
require_params[:avatar] = model_params[:avatar].present? ? UploadIO.new(model_params[:avatar].tempfile, model_params[:avatar].content_type, model_params[:avatar].original_filename) : nil
require_params
end


require 'net/http/post/multipart'


url = URI.parse('http://www.example.com/upload')
Net::HTTP.start(url.host, url.port) do |http|
req = Net::HTTP::Post::Multipart.new(url, model_params)
key = "authorization_key"
req.add_field("Authorization", key) #add to Headers
http.use_ssl = (url.scheme == "https")
http.request(req)
end

Https://github.com/feuda/multipart-post/tree/patch-1

快进到2017年,ruby stdlib net/http从1.9.3开始就内置了这个功能

Net: : HTTPRequest # set _ form) : 增加了对 application/x-www-form-urlencode 和 multipart/form-data 的支持。

Https://ruby-doc.org/stdlib-2.3.1/libdoc/net/http/rdoc/net/httpheader.html#method-i-set_form

我们甚至可以使用不支持 :sizeIO来流化表单数据。

希望这个答案能真正帮到某人:)

另外,我只在 Ruby 2.3.1中测试过

另一种只使用标准库的方法是:

uri = URI('https://some.end.point/some/path')
request = Net::HTTP::Post.new(uri)
request['Authorization'] = 'If you need some headers'
form_data = [['photos', photo.tempfile]] # or File.open() in case of local file


request.set_form form_data, 'multipart/form-data'
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| # pay attention to use_ssl if you need it
http.request(request)
end

我尝试了很多方法,但只有这个方法对我有效。

使用 http.rb gem:

HTTP.post("https://here-you-go.com/upload",
form: {
file: HTTP::FormData::File.new(file_path)
})

Details

哈哈,看来没有宝石就这么做是个守口如瓶的秘密。

我使用了 HTTParty gem:

HTTParty.post(
'http://localhost:3000/user',
body: {
name: 'Foo Bar',
email: 'example@email.com',
avatar: File.open('/full/path/to/avatar.jpg')
}
)

Https://github.com/jnunemaker/httparty/blob/master/examples/multipart.rb

Https://github.com/jnunemaker/httparty

gem install httparty