Je veux faire un HTTP POST qui ressemble à un formulaire HMTL posté à partir d'un navigateur. Plus précisément, publiez des champs de texte et un champ de fichier.
La publication de champs de texte est simple, il existe un exemple dans les rdocs net/http, mais je ne vois pas comment poster un fichier avec.
Net :: HTTP ne semble pas être la meilleure idée. freiner a bonne mine.
J'aime RestClient . Il encapsule net/http avec des fonctionnalités intéressantes telles que des données de formulaire en plusieurs parties:
require 'rest_client'
RestClient.post('http://localhost:3000/foo',
:name_of_file_param => File.new('/path/to/file'))
Il prend également en charge le streaming.
gem install rest-client
va vous aider à démarrer.
Je ne peux pas dire assez de bonnes choses à propos de la bibliothèque multipart-post de Nick Sieger.
Il ajoute la prise en charge de la publication en plusieurs parties directement dans Net :: HTTP, vous évitant ainsi de vous soucier manuellement des limites ou des grandes bibliothèques pouvant avoir des objectifs différents des vôtres.
Voici un petit exemple sur la façon de l’utiliser à partir du 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
Vous pouvez consulter la bibliothèque ici: http://github.com/nicksieger/multipart-post
ou l'installer avec:
$ Sudo gem install multipart-post
Si vous vous connectez via SSL, vous devez démarrer la connexion de la manière suivante:
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
semble être une excellente solution, mais si cela ne répond pas à vos besoins, vous pouvez le faire avec Net::HTTP
. Une publication de formulaire en plusieurs parties est juste une chaîne soigneusement formatée avec des en-têtes supplémentaires. Il semble que tous les programmeurs Ruby qui ont besoin de publier des articles en plusieurs parties finissent par écrire leur propre petite bibliothèque, ce qui me fait me demander pourquoi cette fonctionnalité n'est pas intégrée. Peut-être que c'est ... Quoi qu'il en soit, pour le plaisir de votre lecture, je vais donner la solution ici. Ce code est basé sur des exemples trouvés sur quelques blogs, mais je regrette de ne plus pouvoir trouver les liens. Donc, je suppose que je dois juste prendre tout le crédit pour moi-même ...
Le module que j'ai écrit pour cela contient une classe publique, permettant de générer les données de formulaire et les en-têtes à partir d'un hachage d'objets String
et File
. Ainsi, par exemple, si vous souhaitez publier un formulaire avec un paramètre de chaîne nommé "title" et un paramètre de fichier nommé "document", procédez comme suit:
#prepare the query
data, headers = Multipart::Post.prepare_query("title" => my_string, "document" => my_file)
Alors vous faites juste une POST
normale avec Net::HTTP
:
http = Net::HTTP.new(upload_uri.Host, upload_uri.port)
res = http.start {|con| con.post(upload_uri.path, data, headers) }
Ou bien si vous voulez faire la POST
. Le fait est que Multipart
renvoie les données et les en-têtes que vous devez envoyer. Et c'est tout! Simple, non? Voici le code du module Multipart (vous avez besoin de la gemme 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:[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
Voici ma solution après avoir essayé d'autres solutions disponibles sur ce post, je l'utilise pour télécharger des photos sur 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
Un autre utilisant uniquement des bibliothèques standard:
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
J'ai essayé beaucoup d'approches mais seulement cela a fonctionné pour moi.
Ok, voici un exemple simple en utilisant curb.
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 rapide jusqu'en 2017, Ruby
stdlib
net/http
possède cette fonction intégrée depuis le 1.9.3
Net :: HTTPRequest # set_form): Ajouté pour prendre en charge à la fois application/x-www-form-urlencoded et multipart/form-data.
https://Ruby-doc.org/stdlib-2.3.1/libdoc/net/http/rdoc/Net/HTTPHeader.html#method-i-set_form
Nous pouvons même utiliser IO
qui ne supporte pas :size
pour diffuser les données de formulaire.
En espérant que cette réponse puisse vraiment aider quelqu'un :)
P.S. Je n'ai testé cela que dans Ruby 2.3.1
restclient n'a pas fonctionné pour moi jusqu'à ce que je substitue create_file_field dans RestClient :: Payload :: Multipart.
Il créait un 'Contenu-Disposition: multipart/form-data' dans chaque partie où il devrait être ‘Content-Disposition: form-data '.
http://www.ietf.org/rfc/rfc2388.txt
Ma fourchette est là si vous en avez besoin: [email protected]: kcrawford/rest-client.git
il y a aussi nick sieger's multipart-post à ajouter à la liste de solutions possibles
Eh bien, la solution avec NetHttp présente un inconvénient: lors de la publication de gros fichiers, elle charge d’abord le fichier entier en mémoire.
Après avoir joué un peu avec elle, j'ai proposé la solution suivante:
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
J'ai eu le même problème (besoin de poster sur le serveur Web jboss). Curb fonctionne bien pour moi, sauf que cela a provoqué un crash de Ruby (Ruby 1.8.7 sur Ubuntu 8.10) lorsque j'utilise des variables de session dans le code.
Je creuse dans les documents rest-client, n'a pas pu trouver d'indication de support multipart. J'ai essayé les exemples rest-client ci-dessus mais jboss a dit que la publication http n'est pas en plusieurs parties.
Le joyau multipart-post fonctionne plutôt bien avec Rails 4 Net :: HTTP, aucun autre joyau spécial
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