Ruby: Wie poste ich eine Datei über HTTP als Multipart / Formulardaten?

112

Ich möchte einen HTTP-POST durchführen, der aussieht wie ein HMTL-Formular, das von einem Browser gesendet wird. Veröffentlichen Sie insbesondere einige Textfelder und ein Dateifeld.

Das Posten von Textfeldern ist unkompliziert. Es gibt ein Beispiel genau dort im Netz / http rdocs, aber ich kann nicht herausfinden, wie eine Datei zusammen mit diesen Feldern gepostet werden soll.

Net :: HTTP scheint nicht die beste Idee zu sein. Bordstein sieht gut aus.

kch
quelle

Antworten:

102

Ich mag RestClient . Es kapselt net / http mit coolen Funktionen wie mehrteiligen Formulardaten:

require 'rest_client'
RestClient.post('http://localhost:3000/foo', 
  :name_of_file_param => File.new('/path/to/file'))

Es unterstützt auch Streaming.

gem install rest-client wird Ihnen den Einstieg erleichtern.

Pedro
quelle
Ich nehme das zurück, Datei-Uploads funktionieren jetzt. Das Problem, das ich jetzt habe, ist, dass der Server eine 302 gibt und der Rest-Client dem RFC folgt (was kein Browser tut) und eine Ausnahme auslöst (da Browser vor diesem Verhalten warnen sollen). Die andere Alternative ist Bordstein, aber ich hatte noch nie Glück, Bordstein in Windows zu installieren.
Matt Wolfe
7
Die API hat sich seit dem ersten Posten ein wenig geändert. Multipart wird jetzt wie folgt aufgerufen: RestClient.post ' localhost: 3000 / foo ' ,: upload => File.new ('/ path / tofile')) Siehe github.com/ Archiloque / Rest-Client für weitere Details.
Clinton
2
rest_client unterstützt die Bereitstellung von Anforderungsheadern nicht. Viele REST-Anwendungen erfordern / erwarten einen bestimmten Headertyp, sodass der Restclient in diesem Fall nicht funktioniert. Zum Beispiel benötigt JIRA ein Token X-Atlassian-Token.
Onknows
Ist es möglich, den Fortschritt beim Hochladen von Dateien zu erhalten? zB 40% werden hochgeladen.
Ankush
1
+1 zum Hinzufügen der gem install rest-clientund require 'rest_client'Teile. Diese Information wird von zu vielen Rubinbeispielen weggelassen.
Dansalmo
36

Ich kann nicht genug gute Dinge über die mehrteilige Post-Bibliothek von Nick Sieger sagen.

Es bietet Unterstützung für mehrteiliges Posten direkt in Net :: HTTP, sodass Sie sich nicht mehr manuell um Grenzen oder große Bibliotheken kümmern müssen, die möglicherweise andere Ziele als Ihre eigenen haben.

Hier ist ein kleines Beispiel zur Verwendung in der README-Datei :

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

Sie können die Bibliothek hier überprüfen: http://github.com/nicksieger/multipart-post

oder installieren Sie es mit:

$ sudo gem install multipart-post

Wenn Sie eine Verbindung über SSL herstellen, müssen Sie die Verbindung wie folgt starten:

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
quelle
3
Dieser hat es für mich getan, genau das, wonach ich gesucht habe und genau das, was enthalten sein sollte, ohne dass ein Edelstein benötigt wird. Ruby ist so weit vorne und doch so weit hinten.
Trey
Super, das kommt wie ein Gott sendet! hat dies verwendet, um das OAuth-Juwel zur Unterstützung des Hochladens von Dateien zu monkeypatchen. Ich habe nur 5 Minuten gebraucht.
Matthias
@matthias Ich versuche ein Foto mit OAuth Gem hochzuladen, bin aber gescheitert. Könntest du mir ein Beispiel für dein Monkeypatch geben?
Hooopo
1
Der Patch war ziemlich spezifisch für mein Skript (schnell und schmutzig), aber sehen Sie es sich an und vielleicht können Sie einen allgemeineren Ansatz finden ( gist.github.com/974084 )
Matthias
3
Multipart unterstützt keine Anforderungsheader. Wenn Sie beispielsweise die JIRA REST-Schnittstelle verwenden möchten, ist Multipart nur eine Verschwendung wertvoller Zeit.
Onknows
30

curbsieht nach einer großartigen Lösung aus, aber falls sie nicht Ihren Anforderungen entspricht, können Sie dies tun Net::HTTP. Ein mehrteiliger Formularbeitrag ist nur eine sorgfältig formatierte Zeichenfolge mit einigen zusätzlichen Überschriften. Es scheint, als würde jeder Ruby-Programmierer, der mehrteilige Posts erstellen muss, seine eigene kleine Bibliothek dafür schreiben, weshalb ich mich frage, warum diese Funktionalität nicht integriert ist. Vielleicht ist es ... Wie auch immer, für Ihr Lesevergnügen werde ich hier meine Lösung geben. Dieser Code basiert auf Beispielen, die ich in einigen Blogs gefunden habe, aber ich bedauere, dass ich die Links nicht mehr finden kann. Also muss ich wohl alle Ehre für mich selbst aufbringen ...

Das Modul, das ich dafür geschrieben habe, enthält eine öffentliche Klasse zum Generieren der Formulardaten und Header aus einem Hash von Stringund FileObjekten. Wenn Sie beispielsweise ein Formular mit einem Zeichenfolgenparameter namens "title" und einem Dateiparameter mit dem Namen "document" veröffentlichen möchten, gehen Sie wie folgt vor:

#prepare the query
data, headers = Multipart::Post.prepare_query("title" => my_string, "document" => my_file)

Dann machst du einfach ein normales POSTmit Net::HTTP:

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

Oder wie auch immer Sie das tun möchten POST. Der Punkt ist, dass Multipartdie Daten und Header zurückgegeben werden, die Sie senden müssen. Und das ist es! Einfach, richtig? Hier ist der Code für das Multipart-Modul (Sie benötigen das mime-typesJuwel):

# 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
quelle
Hallo! Wie lautet die Lizenz für diesen Code? Außerdem: Es könnte schön sein, die URL für diesen Beitrag in den Kommentaren oben hinzuzufügen. Vielen Dank!
docwhat
5
Der Code in diesem Beitrag ist unter der WTFPL ( sam.zoy.org/wtfpl ) lizenziert . Genießen!
Cody Brimhall
Sie sollten den Dateistream nicht an den Initialisierungsaufruf der FileParamKlasse übergeben. Die Zuweisung in der to_multipartMethode kopiert den Dateiinhalt erneut, was nicht erforderlich ist! Statt nur den Dateideskriptor geben und von ihm eingelesento_multipart
mober
1
Dieser Code ist großartig! Weil es funktioniert. Rest-Client und Siegers Multipart-Post unterstützen keine Anforderungsheader. Wenn Sie Anforderungsheader benötigen, verschwenden Sie viel wertvolle Zeit mit Rest-Client- und Siegers Multipart-Posts.
Onknows
Tatsächlich unterstützt @Onno jetzt Anforderungsheader. Siehe meinen Kommentar zu Eric's Antwort
Alexanderbird
24

Eine andere, die nur Standardbibliotheken verwendet:

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

Versuchte viele Ansätze, aber nur das wurde für mich gearbeitet.

Vladimir Rozhkov
quelle
3
Danke dafür. Ein kleiner Punkt, Linie 1 sollte sein: uri = URI('https://some.end.point/some/path') So können Sie anrufen uri.portund uri.hostohne Fehler später.
Davidkovsky
1
Eine kleine Änderung, wenn nicht tempfile und Sie eine Datei von Ihrer CD hochladen möchten, sollten Sie File.opennicht verwendenFile.read
Anil Yanduri
1
In den meisten Fällen ist ein Dateiname erforderlich. In diesem Formular habe ich Folgendes hinzugefügt: form_data = [['Datei', File.read (Dateiname), {Dateiname: Dateiname}]]
ZsJoska
4
Das ist die richtige Antwort. Menschen sollten die Verwendung von Wrapper-Edelsteinen nach Möglichkeit einstellen und zu den Grundlagen zurückkehren.
Carlos Roque
18

Hier ist meine Lösung, nachdem ich andere in diesem Beitrag verfügbare ausprobiert habe. Ich verwende sie, um ein Foto auf TwitPic hochzuladen:

  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
quelle
1
Obwohl es ein bisschen hackisch wirkt, ist dies wahrscheinlich die schönste Lösung für mich. Vielen Dank für diesen Vorschlag!
Bo Jeanes
Nur eine Anmerkung für Unvorsichtige, das Medium = @ ... macht Curl so, dass ... eine Datei und nicht nur eine Zeichenfolge ist. Ein bisschen verwirrend mit der Ruby-Syntax, aber @ # {photo.path} ist nicht dasselbe wie #{@photo.path}. Diese Lösung ist eine der besten imho.
Evgeny
7
Das sieht gut aus, aber wenn Ihr @Benutzername "foo && rm -rf /" enthält, wird dies ziemlich schlecht :-P
gaspard
8

Schneller Vorlauf bis 2017, ruby stdlib net/httphat diesen seit 1.9.3 eingebaut

Net :: HTTPRequest # set_form): Wurde hinzugefügt, um sowohl application / x-www-form-urlencoded als auch multipart / form-data zu unterstützen.

https://ruby-doc.org/stdlib-2.3.1/libdoc/net/http/rdoc/Net/HTTPHeader.html#method-i-set_form

Wir können sogar verwenden, IOwas das :sizeStreamen der Formulardaten nicht unterstützt .

Ich hoffe, dass diese Antwort wirklich jemandem helfen kann :)

PS Ich habe dies nur in Ruby 2.3.1 getestet

airmanx86
quelle
7

Ok, hier ist ein einfaches Beispiel mit Bordstein.

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
quelle
3

restclient hat bei mir nicht funktioniert, bis ich create_file_field in RestClient :: Payload :: Multipart überschrieben habe.

Es wurde eine "Inhaltsdisposition: mehrteilige / Formulardaten" in jedem Teil erstellt, in dem es sich um "Inhaltsdisposition: Formulardaten" handeln sollte .

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

Meine Gabel ist hier, wenn Sie sie brauchen: [email protected]: kcrawford / rest-client.git


quelle
Dies ist im neuesten Restclient behoben.
1

Nun, die Lösung mit NetHttp hat den Nachteil, dass beim Posten großer Dateien zuerst die gesamte Datei in den Speicher geladen wird.

Nachdem ich ein bisschen damit gespielt hatte, kam ich auf die folgende Lösung:

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

quelle
Was ist die Klasse StreamPart?
Marlin Pierce
1

Es gibt auch den mehrteiligen Beitrag von Nick Sieger, der der langen Liste möglicher Lösungen hinzugefügt werden kann.

Jan Berkel
quelle
1
multipart-post unterstützt keine Anforderungsheader.
Onknows
Tatsächlich unterstützt @Onno jetzt Anforderungsheader. Siehe meinen Kommentar zu Eric's Antwort
Alexanderbird
0

Ich hatte das gleiche Problem (muss auf dem jboss-Webserver posten). Curb funktioniert gut für mich, außer dass es Ruby zum Absturz gebracht hat (Ruby 1.8.7 unter Ubuntu 8.10), wenn ich Sitzungsvariablen im Code verwende.

Ich stöbere in den Rest-Client-Dokumenten und konnte keinen Hinweis auf mehrteilige Unterstützung finden. Ich habe die obigen Rest-Client-Beispiele ausprobiert, aber jboss sagte, der http-Beitrag sei nicht mehrteilig.


quelle
0

Das Multipart-Post-Juwel funktioniert ziemlich gut mit Rails 4 Net :: HTTP, keinem anderen speziellen Juwel

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
quelle