Ruby: Comment publier un fichier via HTTP en multipart / form-data?

Je veux faire un HTTP POST qui ressemble à un formulaire HMTL publié à partir d’un navigateur. Spécifiquement, publiez des champs de texte et un champ de fichier.

Publier des champs de texte est simple, il y a un exemple dans le net / http rdocs, mais je n’arrive pas à comprendre comment publier un fichier avec lui.

Net :: HTTP ne ressemble pas à la meilleure idée. le trottoir semble bon.

J’aime RestClient . Il encapsule net / http avec des fonctionnalités intéressantes telles que les 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 vous permettra de démarrer.

Je ne peux pas dire assez de bonnes choses à propos de la bibliothèque multipartite de Nick Sieger.

Il ajoute la prise en charge de la publication en plusieurs parties directement sur Net :: HTTP, éliminant ainsi le besoin de vous soucier manuellement des limites ou des grandes bibliothèques qui peuvent avoir des objectives différents des vôtres.

Voici un petit exemple d’utilisation du fichier 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 installez-le avec:

 $ sudo gem install multipart-post 

Si vous vous connectez via SSL, vous devez démarrer la connexion comme ceci:

 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 ressemble à une excellente solution, mais si elle ne répond pas à vos besoins, vous pouvez le faire avec Net::HTTP . Un post de formulaire multipart 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 faire des publications en plusieurs parties finissent par écrire leur propre petite bibliothèque, ce qui me permet de me demander pourquoi cette fonctionnalité n’est pas intégrée. Peut-être que c’est … En tout cas, pour votre plaisir de lecture, je vais aller de l’avant et donner ma solution ici. Ce code est basé sur des exemples trouvés sur plusieurs 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 …

Le module que j’ai écrit pour cela contient une classe publique, pour générer les données de formulaire et les en-têtes à partir d’un hachage d’objects Ssortingng et File . 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_ssortingng, "document" => my_file) 

Ensuite, il vous suffit de faire un POST normal 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 si vous voulez faire le 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 du gem mime-types ):

 # Takes a hash of ssortingng and file parameters and returns a ssortingng of text # formatted to be sent as a multipart form post. # # Author:: Cody Brimhall  # Created:: 22 Feb 2008 # License:: Dissortingbuted 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 :ssortingng or :read messages, then it is # interpreted as a file and processed accordingly; otherwise, it is assumed # to be a ssortingng 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(SsortingngParam.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 ssortingng key/value pair for inclusion with a multipart post class SsortingngParam 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 ssortingng 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 disponibles sur ce post, je l’utilise pour télécharger une photo 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 

Ok, voici un exemple simple 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] 

Un autre utilisant uniquement des bibliothèques standard:

 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.read()/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 

Essayé beaucoup d’approches mais seulement cela a été travaillé pour moi.

restclient n’a pas fonctionné pour moi jusqu’à ce que je surclasse create_file_field dans RestClient :: Payload :: Multipart.

Il créait un “Content-Disposition: multipart / form-data” dans chaque partie où il devrait être “Content-Disposition: form-data” .

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

Mon fork est là si vous en avez besoin: git@github.com: kcrawford / rest-client.git

Avance rapide jusqu’en 2017, ruby stdlib net/http stdlib ce logiciel 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 du formulaire.

En espérant que cette réponse puisse vraiment aider quelqu’un 🙂

PS Je l’ai seulement testé dans Ruby 2.3.1

Eh bien, la solution avec NetHttp présente un inconvénient: lors de la publication de gros fichiers, elle charge tout le fichier en mémoire en premier.

Après avoir joué un peu avec elle, je suis arrivé à 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 

il y a aussi la publication multipartite de nick sieger à append à la longue liste de solutions possibles.

J’ai eu le même problème (besoin de publier 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 fouille dans les documents rest-client, n’a pas pu trouver d’indication de prise en charge multipartie. J’ai essayé les exemples de client restant ci-dessus, mais jboss a déclaré que la publication http n’est pas multipartie.

Le joyau multipart-post fonctionne assez 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 

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