Ruby: Como publicar um arquivo via HTTP como multipart / form-data?
Pergunta
Eu quero fazer um HTTP POST que se parece com um formulário de HMTL postou a partir de um browser. Especificamente, postar alguns campos de texto e um campo de arquivo.
Publicação de campos de texto é simples, há um exemplo bem ali na net / http RDocs, mas eu não consigo descobrir como postar um arquivo junto com ele.
Net :: HTTP não parece ser a melhor idéia. calçada é bom olhar.
Solução
Eu gosto restclient . Ele encapsula net / http com recursos interessantes como dados de formulários com várias partes:
require 'rest_client'
RestClient.post('http://localhost:3000/foo',
:name_of_file_param => File.new('/path/to/file'))
Ele também suporta streaming.
gem install rest-client
irá ajudar a começar.
Outras dicas
Eu não posso dizer bastante coisas boas sobre biblioteca multipart-post de Nick Sieger.
Ele adiciona suporte para postagem multipart diretamente a Net :: HTTP, removendo a sua necessidade de preocupação manualmente sobre fronteiras ou grandes bibliotecas que podem ter objetivos diferentes do que o seu próprio.
Aqui está um pequeno exemplo de como usá-lo a partir do README :
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
Você pode conferir a biblioteca aqui: http://github.com/nicksieger/multipart-post
ou instalá-lo com:
$ sudo gem install multipart-post
Se você está se conectando através de SSL que você precisa para iniciar a conexão assim:
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|
aparência curb
como uma grande solução, mas no caso de ele não atender às suas necessidades, você pode fazê-lo com Net::HTTP
. Um post formulário de várias é apenas uma seqüência cuidadosamente formatado com alguns cabeçalhos extra. Parece que todo programador rubi que precisa para fazer mensagens de várias extremidades de escrever sua própria biblioteca pequena para ele, o que me faz pensar porque essa funcionalidade não é built-in. Talvez ele é ... De qualquer forma, para seu prazer da leitura, eu vou em frente e dar a minha solução aqui. Este código é baseado fora de exemplos que eu encontrei em um par de blogs, mas lamento que eu não posso encontrar os links mais. Então eu acho que eu só tenho que levar todo o crédito para mim ...
O módulo I escreveu para este contém uma classe pública, para gerar os dados do formulário e cabeçalhos de um hash de String
e File
objetos. Assim, por exemplo, se você queria postar um formulário com um parâmetro de cadeia chamado "título" e um parâmetro de arquivo chamado "documento", você faria o seguinte:
#prepare the query
data, headers = Multipart::Post.prepare_query("title" => my_string, "document" => my_file)
Em seguida, você acabou de fazer um POST
normal com Net::HTTP
:
http = Net::HTTP.new(upload_uri.host, upload_uri.port)
res = http.start {|con| con.post(upload_uri.path, data, headers) }
Ou no entanto outra coisa que você quer fazer o POST
. O ponto é que Multipart
retorna os dados e cabeçalhos que você precisa para enviar. E é isso! Simples, certo? Aqui está o código para o módulo Multipart (você precisa da gema mime-types
):
# 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
Aqui está a minha solução depois de tentar outras disponíveis sobre este post, eu estou usando-o para fazer o upload da foto no 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
Outra usando bibliotecas único padrão:
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
tentei um monte de abordagens, mas só isso foi trabalhado para mim.
Ok, aqui está um exemplo simples usando meio-fio.
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]
Fast forward para 2017, ruby
stdlib
net/http
tem este built-in desde 1.9.3
Net :: HTTPRequest # set_form):. Soma-se a apoiar tanto application / x-www-form-urlencoded e multipart / form-data ??p>
https://ruby-doc.org/stdlib-2.3.1/libdoc/net/http/rdoc/Net/HTTPHeader.html#method-i-set_form
Podemos até usar IO
que não suporta :size
para transmitir os dados do formulário.
Na esperança de que esta resposta pode realmente ajudar alguém:)
P.S. Eu só testei isso em Ruby 2.3.1
restclient não funcionou para mim até que eu superou quaisquer create_file_field em restclient :: Payload :: Multipart.
Foi criando um 'Content-Disposition: multipart / form-data' em cada parte onde ele deve ser 'Content-Disposition: form-data'.
http://www.ietf.org/rfc/rfc2388.txt
O meu garfo é aqui se você precisar dele: git@github.com: kcrawford / rest-client.git
Bem a solução com NetHttp tem uma desvantagem que é quando postar arquivos grandes ele carrega o arquivo inteiro na memória em primeiro lugar.
Depois de jogar um pouco com ele eu vim com a seguinte solução:
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
Eu tive o mesmo problema (necessidade de adicionar ao servidor web jboss). Curb funciona bem para mim, exceto que ele causou rubi de acidente (ruby 1.8.7 no Ubuntu 8.10), quando eu usar variáveis ??de sessão no código.
Eu cavar a documentação restante para o cliente, não poderia encontrar indicação de suporte multipart. Eu tentei os exemplos resto-cliente acima, mas jboss disse o post http não é multipart.
A jóia multipart-post funciona muito bem com Rails 4 Net :: HTTP, nenhuma outra jóia especial
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