web-dev-qa-db-fra.com

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

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.

96
kch

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.

91
Pedro

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|
35
eric

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
29
Cody Brimhall

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
18
Alex

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.

8
Vladimir Rozhkov

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]
8
kch

Avance rapide jusqu'en 2017, Rubystdlibnet/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

5
airmanx86

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

3
user243633

il y a aussi nick sieger's multipart-post à ajouter à la liste de solutions possibles 

1
Jan Berkel

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
1

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.

0
zd

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

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

0
Feuda