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

113

Je veux faire un HTTP POST qui ressemble à un formulaire HMTL publié à partir d'un navigateur. Plus précisément, publiez des champs de texte et un champ de fichier.

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

Net :: HTTP ne semble pas être la meilleure idée. le trottoir a l' air bien.

kch
la source

Réponses:

103

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 aidera à démarrer.

Pedro
la source
Je reprends ça, les téléchargements de fichiers fonctionnent maintenant. Le problème que j'ai maintenant est que le serveur donne un 302 et le reste-client suit la RFC (ce qu'aucun navigateur ne fait) et lève une exception (puisque les navigateurs sont censés avertir de ce comportement). L'autre alternative est le trottoir mais je n'ai jamais eu de chance d'installer trottoir dans les fenêtres.
Matt Wolfe
7
L'API a un peu changé depuis sa première publication, multipart now est appelé comme: RestClient.post ' localhost: 3000 / foo ',: upload => File.new ('/ path / tofile')) Voir github.com/ archiloque / rest-client pour plus de détails.
Clinton
2
rest_client ne prend pas en charge la fourniture d'en-têtes de requête. De nombreuses applications REST nécessitent / attendent un type d'en-têtes spécifique, donc le client de repos ne fonctionnera pas dans ce cas. Par exemple, JIRA nécessite un token X-Atlassian-Token.
connaît le
Est-il possible d'obtenir la progression du téléchargement du fichier? par exemple, 40% sont téléchargés.
Ankush
1
+1 pour ajouter les parties gem install rest-clientet require 'rest_client'. Cette information est omise d'un trop grand nombre d'exemples de rubis.
dansalmo
36

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

Il ajoute la prise en charge de la publication en plusieurs parties directement sur Net :: HTTP, vous évitant ainsi de vous soucier manuellement des limites ou des grandes bibliothèques qui peuvent 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 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|
Eric
la source
3
Celui-là l'a fait pour moi, exactement ce que je cherchais et exactement ce qui devrait être inclus sans avoir besoin d'un bijou. Ruby est si loin devant, mais si loin derrière.
Trey
génial, cela vient comme un envoi de Dieu! utilisé ceci pour monkeypatch le gem OAuth pour prendre en charge les téléchargements de fichiers. m'a pris seulement 5 minutes.
Matthias
@matthias J'essaye de télécharger une photo avec la gemme OAuth, mais j'ai échoué. pouvez-vous me donner un exemple de votre monkeypatch?
Hooopo
1
Le correctif était assez spécifique à mon script (rapide et sale), mais jetez-y un coup d'œil et peut-être que vous pouvez en utiliser une avec une approche plus générique ( gist.github.com/974084 )
Matthias
3
Multipart ne prend pas en charge les en-têtes de demande. Ainsi, si vous souhaitez par exemple utiliser l'interface JIRA REST, le multipart ne sera qu'une perte de temps précieux.
onknows le
30

curbsemble être une excellente solution, mais au cas où elle ne répondrait 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 quelques en-têtes supplémentaires. Il semble que chaque programmeur Ruby qui a besoin de publier des articles en plusieurs parties finisse par écrire sa 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 votre plaisir de lecture, je vais vous donner ma solution ici. Ce code est basé sur des exemples que j'ai trouvés sur quelques blogs, mais je regrette de ne plus pouvoir trouver les liens. Alors je suppose que je dois juste prendre tout le mérite pour moi-même ...

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' objets Stringet File. Ainsi, par exemple, si vous souhaitez publier un formulaire avec un paramètre de chaîne nommé "titre" 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)

Ensuite, vous faites juste une normale POSTavec Net::HTTP:

http = Net::HTTP.new(upload_uri.host, upload_uri.port)
res = http.start {|con| con.post(upload_uri.path, data, headers) }

Ou quelle que soit la manière dont vous souhaitez effectuer le POST. Le fait est que Multipartrenvoie 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 mime-typesgem):

# 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
la source
Salut! Quelle est la licence sur ce code? Aussi: Il pourrait être intéressant d'ajouter l'URL de ce message dans les commentaires en haut. Merci!
docwhat
5
Le code de cet article est sous licence WTFPL ( sam.zoy.org/wtfpl ). Prendre plaisir!
Cody Brimhall
vous ne devez pas passer le filestream dans l'appel initialize de la FileParamclasse. L'affectation dans la to_multipartméthode copie à nouveau le contenu du fichier, ce qui n'est pas nécessaire! Au lieu de cela, passez uniquement le descripteur de fichier et lisez-le dansto_multipart
mober
1
Ce code est génial! Parce que ça marche. Rest-client et Siegers Multipart-post NE prennent PAS en charge les en-têtes de demande. Si vous avez besoin d'en-têtes de demande, vous perdrez beaucoup de temps précieux avec rest-client et Siegers Multipart post.
onknows le
En fait, @Onno, il prend désormais en charge les en-têtes de requête. Voir mon commentaire sur la réponse d'
Eric
24

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 cela a fonctionné pour moi.

Vladimir Rozhkov
la source
3
Merci pour cela. Un point mineur, la ligne 1 devrait être: De uri = URI('https://some.end.point/some/path') cette façon, vous pouvez appeler uri.portet uri.hostsans erreur plus tard.
davidkovsky
1
un changement mineur, sinon tempfile et que vous souhaitez télécharger un fichier à partir de votre disque, vous ne devriez File.openpas l' utiliserFile.read
Anil Yanduri
1
dans la plupart des cas, un nom de fichier est requis, c'est le formulaire que j'ai ajouté: form_data = [['file', File.read (file_name), {filename: file_name}]]
ZsJoska
4
c'est la bonne réponse. les gens devraient cesser d'utiliser les gemmes d'emballage lorsque cela est possible et revenir à l'essentiel.
Carlos Roque
18

Voici ma solution après avoir essayé d'autres disponibles sur cet article, 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
Alex
la source
1
Bien que cela semble un peu hackish, c'est probablement la meilleure solution pour moi, merci beaucoup pour cette suggestion!
Bo Jeanes
Juste une note pour les imprudents, le media = @ ... est ce qui fait de curl quelque chose qui ... est un fichier et pas seulement une chaîne. Un peu déroutant avec la syntaxe ruby, mais @ # {photo.path} n'est pas la même chose que #{@photo.path}. Cette solution est l'une des meilleures à mon humble avis.
Evgeny
7
Cela a l'air bien mais si votre @username contient "foo && rm -rf /", cela devient assez mauvais :-P
gaspard
8

Avance rapide jusqu'en 2017, ruby stdlib net/httpcette fonction est intégrée depuis la version 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 IOce qui ne prend pas en charge :sizepour diffuser les données du formulaire.

En espérant que cette réponse puisse vraiment aider quelqu'un :)

PS je n'ai testé cela que dans ruby ​​2.3.1

aviateurx86
la source
7

Ok, voici un exemple simple d'utilisation de trottoir.

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
la source
3

restclient n'a pas fonctionné pour moi jusqu'à ce que j'écrase 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

Ma fourchette est là si vous en avez besoin: [email protected]: kcrawford / rest-client.git


la source
Ce problème est résolu dans le dernier client de restauration.
1

Eh bien, la solution avec NetHttp a 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, j'ai trouvé 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

la source
Qu'est-ce que la classe StreamPart?
Marlin Pierce
1

il y a aussi de Nick Sieger multipart post à ajouter à la longue liste de solutions possibles.

Jan Berkel
la source
1
multipart-post ne prend pas en charge les en-têtes de demande.
connaît le
En fait, @Onno, il prend désormais en charge les en-têtes de requête. Voir mon commentaire sur la réponse d'
Eric
0

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 causé le 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, je n'ai pas pu trouver d'indication de prise en charge en plusieurs parties. J'ai essayé les exemples rest-client ci-dessus mais jboss a dit que le message http n'est pas en plusieurs parties.


la source
0

La gemme multipart-post fonctionne plutôt bien avec Rails 4 Net :: HTTP, pas d'autre gemme spéciale

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
la source