it-swarm.com.de

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

Ich möchte ein HTTP POST erstellen, das wie ein HMTL-Formular aussieht, das von einem Browser bereitgestellt wird. Veröffentlichen Sie insbesondere einige Textfelder und ein Dateifeld.

Das Versenden von Textfeldern ist unkompliziert. Es gibt ein Beispiel in den net/http-Rdocs, aber ich kann nicht herausfinden, wie eine Datei zusammen mit der Datei gepostet wird.

Net :: HTTP sieht nicht nach der besten Idee aus. curb sieht gut aus.

96
kch

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.

91
Pedro

Ich kann nicht genug Gutes über Nick Siegers multipart-post-Bibliothek sagen.

Dadurch wird die Unterstützung für das mehrteilige Posting direkt in Net :: HTTP unterstützt, sodass Sie sich nicht mehr manuell um Grenzen oder große Bibliotheken kümmern müssen, die andere Ziele haben als Ihre eigenen.

Hier ein kleines Beispiel, wie man es aus der README verwendet:

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 besuchen: http://github.com/nicksieger/multipart-post

oder installiere es mit:

$ Sudo gem install multipart-post

Wenn Sie sich über SSL verbinden, 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|
35
eric

curb sieht nach einer großartigen Lösung aus, aber falls dies nicht Ihren Anforderungen entspricht, können Sie can mit Net::HTTP tun. Ein mehrteiliger Formularposten ist nur eine sorgfältig formatierte Zeichenfolge mit zusätzlichen Kopfzeilen. Es scheint, als würde jeder Ruby-Programmierer, der mehrteilige Posts schreiben muss, seine eigene kleine Bibliothek dafür schreiben, was mich wundert, warum diese Funktionalität nicht eingebaut ist. Vielleicht ist es ... Wie auch immer, zu Ihrem Lesevergnügen werde ich meine Lösung hier angeben. Dieser Code basiert auf Beispielen, die ich in einigen Blogs gefunden habe, aber ich bedaure, dass ich die Links nicht mehr finden kann. Ich denke, ich muss mich einfach alle Ehre machen ...

Das Modul, das ich dafür geschrieben habe, enthält eine öffentliche Klasse zum Generieren der Formulardaten und Header aus einem Hash von String- und File-Objekten. Wenn Sie beispielsweise ein Formular mit einem String-Parameter namens "title" und einem Dateiparameter namens "document" veröffentlichen möchten, würden Sie Folgendes tun:

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

Dann machen Sie einfach ein normales POST mit Net::HTTP:

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

Oder Sie möchten jedoch POST. Der Punkt ist, dass Multipart die Daten und Header zurückgibt, die Sie senden müssen. Und das ist es! Einfach, richtig? Hier ist der Code für das Multipart-Modul (Sie benötigen den mime-types gem):

# 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

Hier ist meine Lösung, nachdem ich andere ausprobiert habe, die in diesem Post verfügbar sind. Ich benutze sie, um ein Foto auf TwitPic hochzuladen:

  def upload(photo)
    `curl -F [email protected]#{photo.path} -F username=#{@username} -F password=#{@password} -F message='#{photo.title}' http://twitpic.com/api/uploadAndPost`
  end
18
Alex

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

Ich habe viele Ansätze ausprobiert, aber nur das hat für mich funktioniert.

8

Ok, hier ist ein einfaches Beispiel mit dem 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]
8
kch

Schneller Vorlauf bis 2017, Rubystdlibnet/http hat dieses seit 1.9.3 eingebaut

Net :: HTTPRequest # set_form): Zur Unterstützung von application/x-www-form-urlencoded und multipart/form-data hinzugefügt.

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

Wir können sogar IO verwenden, das :size nicht unterstützt, um die Formulardaten zu streamen.

Hoffen, dass diese Antwort wirklich jemandem helfen kann :)

P.S. Ich habe das nur in Ruby 2.3.1 getestet

5
airmanx86

restclient funktionierte nicht für mich, bis ich create_file_field in RestClient :: Payload :: Multipart überschrieben habe.

Es wurde ein 'Content-Disposition: Multipart/Form-Data' in jedem Teil erstellt, wo es 'Content-Disposition: Form-Data' sein sollte.

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

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

3
user243633

es gibt auch nick sieger's multipart-post , um die lange Liste möglicher Lösungen hinzuzufügen. 

1
Jan Berkel

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

Nachdem ich ein bisschen damit gespielt hatte, kam ich zu folgender 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

Ich hatte das gleiche Problem (muss auf jboss Webserver gepostet werden). Curb funktioniert gut für mich, außer dass Ruby abstürzt (Ruby 1.8.7 auf Ubuntu 8.10), wenn ich im Code Sitzungsvariablen verwende.

Ich griff in die Rest-Client-Dokumente, konnte keine Hinweise auf Unterstützung für mehrere Teile finden. Ich habe die Rest-Client-Beispiele oben ausprobiert, aber jboss sagte, dass der http-Beitrag nicht mehrteilig ist.

0
zd

Der Multipart-Post-Edelstein funktioniert ziemlich gut mit Rails 4 Net :: HTTP, keinem anderen besonderen Edelstein

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