Ruby: ¿Cómo publicar un archivo a través de HTTP como multipart / form-data?

StackOverflow https://stackoverflow.com/questions/184178

  •  06-07-2019
  •  | 
  •  

Pregunta

Quiero hacer un HTTP POST que se parece a un formulario HMTL publicado desde un navegador. Específicamente, publique algunos campos de texto y un campo de archivo.

Publicar campos de texto es sencillo, hay un ejemplo allí mismo en la red / http rdocs, pero no puedo entender cómo publicar un archivo junto con él.

Net :: HTTP no parece la mejor idea. curb se ve bien.

¿Fue útil?

Solución

Me gusta RestClient . Encapsula net / http con características interesantes como datos de formularios de varias partes:

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

También es compatible con la transmisión.

gem install rest-client lo ayudará a comenzar.

Otros consejos

No puedo decir lo suficiente sobre la biblioteca multiparte de Nick Sieger.

Agrega soporte para la publicación de múltiples partes directamente en Net :: HTTP, eliminando la necesidad de preocuparse manualmente por los límites o las grandes bibliotecas que pueden tener objetivos diferentes a los suyos.

Aquí hay un pequeño ejemplo sobre cómo usarlo en 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

Puede consultar la biblioteca aquí: http://github.com/nicksieger/multipart-post

o instálelo con:

$ sudo gem install multipart-post

Si se conecta a través de SSL, debe iniciar la conexión de esta manera:

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|

curb parece una gran solución, pero en caso de que no satisfaga sus necesidades, puede hacerlo con Net :: HTTP . Una publicación de formulario de varias partes es solo una cadena cuidadosamente formateada con algunos encabezados adicionales. Parece que cada programador de Ruby que necesita hacer publicaciones de varias partes termina escribiendo su propia pequeña biblioteca para ello, lo que me hace preguntarme por qué esta funcionalidad no está incorporada. Tal vez sea ... De todos modos, para su placer de lectura, seguiré adelante y daré mi solución aquí. Este código se basa en ejemplos que encontré en un par de blogs, pero lamento no poder encontrar más los enlaces. Así que supongo que solo tengo que tomar todo el crédito por mí mismo ...

El módulo que escribí para esto contiene una clase pública, para generar los datos y encabezados del formulario a partir de un hash de objetos String y File . Entonces, por ejemplo, si desea publicar un formulario con un parámetro de cadena llamado " title " y un parámetro de archivo llamado "documento", haría lo siguiente:

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

Entonces simplemente haces una POST normal con Net :: HTTP :

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

O como quiera que quiera hacer el POST . El punto es que Multipart devuelve los datos y encabezados que necesita enviar. ¡Y eso es! Simple, verdad? Aquí está el código para el módulo Multiparte (necesita la 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

Aquí está mi solución después de probar otras disponibles en esta publicación, la estoy usando para subir fotos en 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

Otra que usa solo bibliotecas estándar:

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

Intenté muchos enfoques, pero solo esto funcionó para mí.

Ok, aquí hay un ejemplo simple usando bordillo.

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]

Avance rápido hasta 2017, ruby ?? stdlib net / http tiene esto incorporado desde 1.9.3

  

Net :: HTTPRequest # set_form): agregado para admitir tanto application / x-www-form-urlencoded como multipart / form-data.

https://ruby-doc.org/stdlib-2.3.1/libdoc/net/http/rdoc/Net/HTTPHeader.html#method-i-set_form

Incluso podemos usar IO que no admite : size para transmitir los datos del formulario.

Esperando que esta respuesta realmente pueda ayudar a alguien :)

P.S. Solo probé esto en ruby ??2.3.1

restclient no funcionó para mí hasta que anulé create_file_field en RestClient :: Payload :: Multipart.

Estaba creando una 'Disposición de contenido: multiparte / datos de formulario' en cada parte donde debería estar 'Disposición de contenido: datos de formulario' .

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

Mi tenedor está aquí si lo necesita: git@github.com: kcrawford / rest-client.git

Bueno, la solución con NetHttp tiene un inconveniente: cuando se publican archivos grandes, primero se carga todo el archivo en la memoria.

Después de jugar un poco con él, se me ocurrió la siguiente solución:

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

también hay multipart-post de nick sieger para agregar a la larga lista de posibles soluciones.

Tuve el mismo problema (necesito publicar en el servidor web jboss). Curb funciona bien para mí, excepto que causó el bloqueo de ruby ??(ruby 1.8.7 en ubuntu 8.10) cuando uso variables de sesión en el código.

Excavo en los documentos de rest-client, no pude encontrar indicaciones de soporte multiparte. Probé los ejemplos anteriores de rest-client pero jboss dijo que la publicación http no es multiparte.

La gema de publicación múltiple funciona bastante bien con Rails 4 Net :: HTTP, ninguna otra gema 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 bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top