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

113

Quiero hacer un HTTP POST que parece 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 net / http rdocs, pero no puedo entender cómo publicar un archivo junto con él.

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

kch
fuente

Respuestas:

103

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 te ayudará a empezar.

Pedro
fuente
Retiro eso, la carga de archivos ahora funciona. El problema que tengo ahora es que el servidor da un 302 y el resto del cliente sigue el RFC (que ningún navegador hace) y lanza una excepción (ya que se supone que los navegadores advierten sobre este comportamiento). La otra alternativa es el bordillo, pero nunca he tenido suerte instalando bordillo en ventanas.
Matt Wolfe
7
La API ha cambiado un poco desde que se publicó por primera vez, ahora se invoca multiparte como: RestClient.post ' localhost: 3000 / foo ',: upload => File.new ('/ path / tofile')) Ver github.com/ archiloque / rest-client para más detalles.
Clinton
2
rest_client no admite el suministro de encabezados de solicitud. Muchas aplicaciones REST requieren / esperan un tipo específico de encabezados, por lo que el cliente de descanso no funcionará en ese caso. Por ejemplo, JIRA requiere un token X-Atlassian-Token.
onknows
¿Es posible obtener el progreso de carga del archivo? por ejemplo, se carga el 40%.
Ankush
1
+1 para agregar las partes gem install rest-clienty require 'rest_client'. Esa información se deja fuera de muchos ejemplos de ruby.
dansalmo
36

No puedo decir lo suficiente sobre la biblioteca de publicaciones de varias partes de Nick Sieger.

Agrega soporte para la publicación de varias 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 de cómo usarlo del 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 instalarlo con:

$ sudo gem install multipart-post

Si se está conectando 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|
Eric
fuente
3
Ese lo hizo por mí, exactamente lo que estaba buscando y exactamente lo que debería incluirse sin la necesidad de una gema. Ruby está muy por delante, pero muy atrás.
Trey
impresionante, esto viene como un envío de Dios! usé esto para parchear la gema OAuth para admitir la carga de archivos. me tomó solo 5 minutos.
Matthias
@matthias Estoy intentando subir una foto con la gema de OAuth, pero no he podido. ¿Podría darme algún ejemplo de su parche de mono?
Hooopo
1
El parche era bastante específico para mi script (rápido y sucio), pero échale un vistazo y tal vez puedas mejorar con un enfoque más genérico ( gist.github.com/974084 )
Matthias
3
Multipart no admite encabezados de solicitud. Por lo tanto, si, por ejemplo, desea utilizar la interfaz JIRA REST, la función multiparte será una pérdida de tiempo valioso.
onknows
30

curbparece 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 todos los programadores de Ruby que necesitan hacer publicaciones de varias partes terminan escribiendo su propia pequeña biblioteca, 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 debo tomarme todo el crédito por mí mismo ...

El módulo que escribí para esto contiene una clase pública, para generar los datos del formulario y los encabezados a partir de un hash de Stringy Fileobjetos. Entonces, por ejemplo, si quisiera publicar un formulario con un parámetro de cadena llamado "título" 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 lo normal POSTcon 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 quieras hacer el POST. El punto es que Multipartdevuelve los datos y los encabezados que necesita enviar. ¡Y eso es! Simple, ¿verdad? Aquí está el código para el módulo Multipart (necesita la mime-typesgema):

# 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:[email protected]>
# 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
Cody Brimhall
fuente
¡Hola! ¿Cuál es la licencia de este código? Además: sería bueno agregar la URL de esta publicación en los comentarios en la parte superior. ¡Gracias!
docwhat
5
El código de esta publicación tiene licencia de WTFPL ( sam.zoy.org/wtfpl ). ¡Disfrutar!
Cody Brimhall
no debe pasar el flujo de archivos a la llamada de inicialización de la FileParamclase. La asignación en el to_multipartmétodo copia el contenido del archivo nuevamente, ¡lo cual es innecesario! En su lugar, pase solo el descriptor de archivo y to_multipart
léalo
1
¡Este código es EXCELENTE! Porque funciona. Rest-client y Siegers Multipart-post NO admiten encabezados de solicitud. Si necesita encabezados de solicitud, perderá mucho tiempo valioso con rest-client y Siegers Multipart post.
onknows
En realidad, @Onno, ahora admite encabezados de solicitud. Vea mi comentario sobre la respuesta de Eric
alexanderbird
24

Otro 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í.

Vladimir Rozhkov
fuente
3
Gracias por esto. Un punto menor, la línea 1 debería ser: De uri = URI('https://some.end.point/some/path') esa manera puede llamar uri.porty uri.hostsin errores más adelante.
davidkovsky
1
un cambio menor, si no es un archivo temporal y desea cargar un archivo desde su disco, debe usar File.opennoFile.read
Anil Yanduri
1
la mayoría de los casos se requiere un nombre de archivo, esta es la forma en que agregué: form_data = [['archivo', File.read (file_name), {filename: file_name}]]
ZsJoska
4
Esta es la respuesta correcta. la gente debe dejar de usar gemas de envoltura cuando sea posible y volver a lo básico.
Carlos Roque
18

Aquí está mi solución después de probar otras disponibles en esta publicación, la estoy usando para cargar una foto 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
Alex
fuente
1
A pesar de parecer un poco hack, esta es probablemente la mejor solución para mí, ¡muchas gracias por esta sugerencia!
Bo Jeanes
Solo una nota para los incautos, los medios = @ ... es lo que hace curl algo que ... es un archivo y no solo una cadena. Un poco confuso con la sintaxis de ruby, pero @ # {photo.path} no es lo mismo que #{@photo.path}. Esta solución es una de las mejores en mi humilde opinión.
Evgeny
7
Este buen aspecto, pero si su nombre de usuario @ contiene "foo && rm-rf /", esto se pone muy mal :-P
gaspard
8

Avance rápido hasta 2017, ruby stdlib net/httptiene 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 lo IOque no es compatible.:size para transmitir los datos del formulario.

Esperando que esta respuesta realmente pueda ayudar a alguien :)

PD: solo probé esto en ruby ​​2.3.1

aviadorx86
fuente
7

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]
kch
fuente
3

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

Estaba creando un 'Content-Disposition: multipart / form-data' en cada parte donde debería ser 'Content-Disposition: form-data' .

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

Mi fork está aquí si lo necesita: [email protected]: kcrawford / rest-client.git


fuente
Esto se corrigió en el último restclient.
1

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

fuente
¿Qué es la clase StreamPart?
Marlin Pierce
1

También está la publicación multiparte de nick sieger para agregar a la larga lista de posibles soluciones.

Jan Berkel
fuente
1
multipart-post no admite encabezados de solicitud.
onknows
En realidad, @Onno, ahora admite encabezados de solicitud. Vea mi comentario sobre la respuesta de Eric
alexanderbird
0

Tuve el mismo problema (necesito publicar en el servidor web jboss). Curb funciona bien para mí, excepto que hizo que ruby ​​se bloqueara (ruby 1.8.7 en ubuntu 8.10) cuando utilizo variables de sesión en el código.

Busqué en los documentos del resto del cliente, no pude encontrar ninguna indicación de soporte multiparte. Probé los ejemplos de rest-client anteriores, pero jboss dijo que la publicación http no es multiparte.


fuente
0

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

Feuda
fuente