Ruby: ¿Cómo publicar un archivo a través de HTTP como multipart / form-data?
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.
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