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.

Foi útil?

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

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

multipart-post de Nick Sieger para adicionar à longa lista de possíveis soluções.

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

https://github.com/Feuda/multipart-post/tree / patch-1

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top